mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-05 16:20:47 +08:00
commit
f069ecdb76
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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:
|
||||
[
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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"})
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
56
ee/query-service/auth/auth.go
Normal file
56
ee/query-service/auth/auth.go
Normal file
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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{}
|
||||
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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)/)',
|
||||
|
@ -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.
|
||||
|
@ -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)": [
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
3
frontend/public/locales/en/apiKeys.json
Normal file
3
frontend/public/locales/en/apiKeys.json
Normal file
@ -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."
|
||||
}
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
@ -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'),
|
||||
);
|
||||
|
@ -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,
|
||||
|
26
frontend/src/api/APIKeys/createAPIKey.ts
Normal file
26
frontend/src/api/APIKeys/createAPIKey.ts
Normal file
@ -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<SuccessResponse<APIKeyProps> | 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;
|
24
frontend/src/api/APIKeys/deleteAPIKey.ts
Normal file
24
frontend/src/api/APIKeys/deleteAPIKey.ts
Normal file
@ -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<SuccessResponse<AllAPIKeyProps> | 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;
|
24
frontend/src/api/APIKeys/getAPIKey.ts
Normal file
24
frontend/src/api/APIKeys/getAPIKey.ts
Normal file
@ -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<SuccessResponse<PayloadProps> | 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;
|
6
frontend/src/api/APIKeys/getAllAPIKeys.ts
Normal file
6
frontend/src/api/APIKeys/getAllAPIKeys.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { AllAPIKeyProps } from 'types/api/pat/types';
|
||||
|
||||
export const getAllAPIKeys = (): Promise<AxiosResponse<AllAPIKeyProps>> =>
|
||||
axios.get(`/pats`);
|
26
frontend/src/api/APIKeys/updateAPIKey.ts
Normal file
26
frontend/src/api/APIKeys/updateAPIKey.ts
Normal file
@ -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<SuccessResponse<PayloadProps> | 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;
|
30
frontend/src/assets/Dashboard/List.tsx
Normal file
30
frontend/src/assets/Dashboard/List.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
function ListIcon({
|
||||
fillColor,
|
||||
}: {
|
||||
fillColor: CSSProperties['color'];
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="30"
|
||||
height="30"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={fillColor}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="8" x2="21" y1="6" y2="6" />
|
||||
<line x1="8" x2="21" y1="12" y2="12" />
|
||||
<line x1="8" x2="21" y1="18" y2="18" />
|
||||
<line x1="3" x2="3.01" y1="6" y2="6" />
|
||||
<line x1="3" x2="3.01" y1="12" y2="12" />
|
||||
<line x1="3" x2="3.01" y1="18" y2="18" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default ListIcon;
|
@ -1,18 +1,48 @@
|
||||
function Table(): JSX.Element {
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
function TableIcon({
|
||||
fillColor,
|
||||
}: {
|
||||
fillColor: CSSProperties['color'];
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M41.0667 0H6.39993C2.87982 0 0 2.87982 0 6.39993V41.6001C0 45.1202 2.87982 48 6.39993 48H41.0667C44.5868 48 47.4667 45.1202 47.4667 41.6001V6.39993C47.4667 2.87982 44.5868 0 41.0667 0ZM44.2669 6.39993V9.60013H32.0002V3.2002H41.0668C42.8268 3.2002 44.267 4.63992 44.267 6.40003L44.2669 6.39993ZM17.6002 9.60013V3.2002H29.8669V9.60013H17.6002ZM29.8669 11.7333V44.8001H17.6002L17.6005 11.7333H29.8669ZM6.40012 3.20011H15.4667V9.60004H3.20001V6.39984C3.20001 4.63983 4.64011 3.20001 6.40022 3.20001L6.40012 3.20011ZM3.19992 41.6003V11.7335H15.4666V44.8003H6.40003C4.64002 44.8003 3.19982 43.3606 3.19982 41.6005L3.19992 41.6003ZM41.0667 44.8001H32.0001V11.7333H44.2668V41.6001C44.2668 43.3601 42.8267 44.7999 41.0666 44.7999L41.0667 44.8001ZM5.33326 18.6666C5.33326 18.08 5.81317 17.6001 6.39983 17.6001H12.2667C12.8534 17.6001 13.3333 18.08 13.3333 18.6666C13.3333 19.2533 12.8534 19.7332 12.2667 19.7332H6.39983C5.81315 19.7332 5.33326 19.2533 5.33326 18.6666ZM13.3333 25.0666C13.3333 25.6533 12.8534 26.1332 12.2667 26.1332H6.39983C5.81315 26.1332 5.33326 25.6532 5.33326 25.0666C5.33326 24.4799 5.81317 24 6.39983 24H12.2667C12.8534 24 13.3333 24.4799 13.3333 25.0666ZM13.3333 31.4665C13.3333 32.0532 12.8534 32.5331 12.2667 32.5331H6.39983C5.81315 32.5331 5.33326 32.0532 5.33326 31.4665C5.33326 30.8798 5.81317 30.3999 6.39983 30.3999H12.2667C12.8534 30.3999 13.3333 30.8798 13.3333 31.4665ZM13.3333 37.8668C13.3333 38.4535 12.8534 38.9334 12.2667 38.9334H6.39983C5.81315 38.9334 5.33326 38.4535 5.33326 37.8668C5.33326 37.2801 5.81317 36.8002 6.39983 36.8002H12.2667C12.8534 36.7999 13.3333 37.2802 13.3333 37.8668ZM19.7332 18.6667C19.7332 18.0801 20.2131 17.6002 20.7998 17.6002H26.6667C27.2534 17.6002 27.7333 18.0801 27.7333 18.6667C27.7333 19.2534 27.2533 19.7333 26.6667 19.7333H20.7998C20.2131 19.7333 19.7332 19.2534 19.7332 18.6667ZM19.7332 25.0667C19.7332 24.48 20.2131 24.0001 20.7998 24.0001H26.6667C27.2534 24.0001 27.7333 24.48 27.7333 25.0667C27.7333 25.6534 27.2533 26.1332 26.6667 26.1332H20.7998C20.2131 26.1332 19.7332 25.6533 19.7332 25.0667ZM19.7332 31.4666C19.7332 30.8799 20.2131 30.4 20.7998 30.4H26.6667C27.2534 30.4 27.7333 30.8799 27.7333 31.4666C27.7333 32.0533 27.2533 32.5332 26.6667 32.5332H20.7998C20.2131 32.5336 19.7332 32.0533 19.7332 31.4666ZM27.7333 37.8669C27.7333 38.4536 27.2533 38.9335 26.6667 38.9335H20.7998C20.2131 38.9335 19.7332 38.4536 19.7332 37.8669C19.7332 37.2802 20.2131 36.8003 20.7998 36.8003H26.6667C27.2534 36.8 27.7333 37.2803 27.7333 37.8669ZM42.1333 18.6668C42.1333 19.2535 41.6534 19.7334 41.0667 19.7334H35.1999C34.6132 19.7334 34.1333 19.2535 34.1333 18.6668C34.1333 18.0802 34.6132 17.6003 35.1999 17.6003H41.0667C41.6534 17.6003 42.1333 18.0802 42.1333 18.6668ZM42.1333 25.0668C42.1333 25.6535 41.6534 26.1333 41.0667 26.1333H35.1999C34.6132 26.1333 34.1333 25.6534 34.1333 25.0668C34.1333 24.4801 34.6132 24.0002 35.1999 24.0002H41.0667C41.6534 24.0002 42.1333 24.4801 42.1333 25.0668ZM42.1333 31.4667C42.1333 32.0534 41.6534 32.5333 41.0667 32.5333H35.1999C34.6132 32.5333 34.1333 32.0534 34.1333 31.4667C34.1333 30.88 34.6132 30.4001 35.1999 30.4001H41.0667C41.6534 30.4001 42.1333 30.88 42.1333 31.4667ZM42.1333 37.867C42.1333 38.4537 41.6534 38.9336 41.0667 38.9336H35.1999C34.6132 38.9336 34.1333 38.4537 34.1333 37.867C34.1333 37.2803 34.6132 36.8004 35.1999 36.8004H41.0667C41.6534 36.8001 42.1333 37.2803 42.1333 37.867Z"
|
||||
fill="#1668DC"
|
||||
d="M12 3V21"
|
||||
stroke={fillColor}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M19 3H5C3.89543 3 3 3.89543 3 5V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V5C21 3.89543 20.1046 3 19 3Z"
|
||||
stroke={fillColor}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M3 9H21"
|
||||
stroke={fillColor}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M3 15H21"
|
||||
stroke={fillColor}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Table;
|
||||
export default TableIcon;
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,19 +1,29 @@
|
||||
function Value(): JSX.Element {
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
function Value({
|
||||
fillColor,
|
||||
}: {
|
||||
fillColor: CSSProperties['color'];
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="68"
|
||||
height="48"
|
||||
viewBox="0 0 68 48"
|
||||
width="30"
|
||||
height="30"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 5.914V42.086C0 43.6542 0.62289 45.1585 1.73183 46.2675C2.84078 47.3771 4.34511 48 5.91329 48H61.5019C63.0701 48 64.5744 47.3771 65.6834 46.2675C66.7923 45.1585 67.4152 43.6542 67.4152 42.086V5.914C67.4152 4.34578 66.7923 2.84152 65.6834 1.73253C64.5744 0.623576 63.0701 0 61.5019 0H5.91329C4.34508 0 2.84082 0.623576 1.73183 1.73253C0.622872 2.84149 0 4.34581 0 5.914ZM63.4735 5.914V42.086C63.4735 42.6092 63.2659 43.1104 62.896 43.4803C62.5261 43.8495 62.0249 44.0571 61.5024 44.0571H5.91382C4.82549 44.0571 3.94277 43.175 3.94277 42.086V5.91403C3.94277 4.8257 4.82553 3.94298 5.91382 3.94298H61.5024C62.0249 3.94298 62.5261 4.15061 62.896 4.52048C63.2659 4.88968 63.4735 5.39148 63.4735 5.914Z"
|
||||
fill="#1554AD"
|
||||
d="M9 3H5C4.46957 3 3.96086 3.21071 3.58579 3.58579C3.21071 3.96086 3 4.46957 3 5V9M9 3H19C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5V9M9 21H19C19.5304 21 20.0391 20.7893 20.4142 20.4142C20.7893 20.0391 21 19.5304 21 19V9M9 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V9"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M13.7695 17.668C10.1016 17.668 7.48828 20.1758 7.48828 23.6094V23.6328C7.48828 26.8438 9.76172 29.2109 13.0078 29.2109C15.3281 29.2109 16.8047 28.0273 17.4258 26.6914H17.6602C17.6602 26.8203 17.6484 26.9492 17.6484 27.0781C17.5195 30.3125 16.3828 32.9375 13.6992 32.9375C12.2109 32.9375 11.168 32.1641 10.7227 30.9805L10.6875 30.8633H7.71094L7.73438 30.9922C8.27344 33.582 10.5938 35.4219 13.6992 35.4219C17.9531 35.4219 20.5195 32.0469 20.5195 26.3516V26.3281C20.5195 20.2344 17.3789 17.668 13.7695 17.668ZM13.7578 26.8906C11.8359 26.8906 10.4414 25.4844 10.4414 23.5273V23.5039C10.4414 21.6172 11.9297 20.1289 13.793 20.1289C15.668 20.1289 17.1328 21.6406 17.1328 23.5742V23.5977C17.1328 25.5078 15.668 26.8906 13.7578 26.8906ZM24.832 35.2344C25.9102 35.2344 26.6953 34.4258 26.6953 33.3828C26.6953 32.3398 25.9102 31.5312 24.832 31.5312C23.7656 31.5312 22.9688 32.3398 22.9688 33.3828C22.9688 34.4258 23.7656 35.2344 24.832 35.2344ZM37.8633 35H40.7578V31.7539H43.0312V29.2578H40.7578V18.0898H36.4805C34.1836 21.582 31.7812 25.4727 29.5898 29.2812V31.7539H37.8633V35ZM32.4023 29.3281V29.1523C34.043 26.2812 36 23.1523 37.7344 20.5039H37.9102V29.3281H32.4023ZM52.6406 35.4219C56.3086 35.4219 58.9219 32.9141 58.9219 29.4805V29.457C58.9219 26.2461 56.6484 23.8789 53.4023 23.8789C51.082 23.8789 49.6055 25.0625 48.9844 26.3984H48.75C48.75 26.2695 48.7617 26.1406 48.7617 26.0117C48.8906 22.7773 50.0273 20.1523 52.7109 20.1523C54.1992 20.1523 55.2422 20.9258 55.6875 22.1094L55.7344 22.2266H58.6992L58.6758 22.0977C58.1367 19.5078 55.8164 17.668 52.7109 17.668C48.457 17.668 45.8906 21.043 45.8906 26.7383V26.7617C45.8906 32.8555 49.0312 35.4219 52.6406 35.4219ZM49.2773 29.5156V29.4922C49.2773 27.582 50.7422 26.1992 52.6523 26.1992C54.5742 26.1992 55.9688 27.6055 55.9688 29.5625V29.5859C55.9688 31.4727 54.4922 32.9609 52.6172 32.9609C50.7422 32.9609 49.2773 31.4492 49.2773 29.5156Z"
|
||||
fill="#1668DC"
|
||||
d="M9.76562 15.6221C9.19922 15.6221 8.78906 15.3047 8.78906 14.6406V14.3818H6.58691C5.83984 14.3818 5.32227 13.9082 5.32227 13.2246C5.32227 12.8486 5.44922 12.4531 5.72266 11.9648C6.18164 11.1396 6.61621 10.4219 7.1582 9.57227C7.74414 8.64941 8.22266 8.33203 9.02832 8.33203C10.0586 8.33203 10.7422 8.90332 10.7422 9.76758V12.7949H10.9033C11.4453 12.7949 11.7041 13.1318 11.7041 13.5908C11.7041 14.0498 11.4404 14.3818 10.8984 14.3818H10.7422V14.6406C10.7422 15.3047 10.3271 15.6221 9.76562 15.6221ZM8.84766 12.8779V9.83594H8.80859C8.10059 10.8711 7.58789 11.6914 7.0166 12.8193V12.8779H8.84766ZM13.2959 15.5C12.6221 15.5 12.3291 15.124 12.3291 14.6064C12.3291 14.2256 12.5049 13.9326 12.8955 13.6055L14.834 11.9453C15.625 11.2666 15.8496 10.959 15.8496 10.5391C15.8496 10.0947 15.5078 9.78711 15.0049 9.78711C14.6338 9.78711 14.3799 9.95801 14.1162 10.3389C13.8428 10.7393 13.5938 10.8857 13.1982 10.8857C12.6709 10.8857 12.3486 10.5781 12.3486 10.0801C12.3486 9.91895 12.3779 9.76758 12.4414 9.62109C12.8125 8.78125 13.8037 8.25879 15.0342 8.25879C16.748 8.25879 17.8418 9.12305 17.8418 10.4023C17.8418 11.3496 17.3535 11.8428 16.2598 12.79L14.9756 13.8984V13.9375H17.2119C17.7295 13.9375 18.0225 14.2451 18.0225 14.7188C18.0225 15.1826 17.7295 15.5 17.2119 15.5H13.2959Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
@ -8,6 +8,7 @@ import { VIEWS } from './constants';
|
||||
export type LogDetailProps = {
|
||||
log: ILog | null;
|
||||
selectedTab: VIEWS;
|
||||
isListViewPanel?: boolean;
|
||||
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
||||
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
|
||||
Pick<DrawerProps, 'onClose'>;
|
||||
|
@ -35,6 +35,7 @@ function LogDetail({
|
||||
onAddToQuery,
|
||||
onClickActionItem,
|
||||
selectedTab,
|
||||
isListViewPanel = false,
|
||||
}: LogDetailProps): JSX.Element {
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const [selectedView, setSelectedView] = useState<VIEWS>(selectedTab);
|
||||
@ -190,6 +191,7 @@ function LogDetail({
|
||||
logData={log}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onClickActionItem}
|
||||
isListViewPanel={isListViewPanel}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
|
||||
|
@ -22,6 +22,10 @@ export const defaultTableStyle: CSSProperties = {
|
||||
maxWidth: '40rem',
|
||||
};
|
||||
|
||||
export const defaultListViewPanelStyle: CSSProperties = {
|
||||
maxWidth: '40rem',
|
||||
};
|
||||
|
||||
export const tableScroll: TableProps<Record<string, unknown>>['scroll'] = {
|
||||
x: true,
|
||||
};
|
||||
|
@ -24,6 +24,7 @@ export type UseTableViewProps = {
|
||||
onClickExpand?: (log: ILog) => void;
|
||||
activeLog?: ILog | null;
|
||||
activeContextLog?: ILog | null;
|
||||
isListViewPanel?: boolean;
|
||||
} & LogsTableViewProps;
|
||||
|
||||
export type ActionsColumnProps = {
|
||||
|
@ -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<Record<string, unknown>> => ({
|
||||
props: {
|
||||
style: getDefaultCellStyle(isDarkMode),
|
||||
style: isListViewPanel
|
||||
? defaultListViewPanelStyle
|
||||
: getDefaultCellStyle(isDarkMode),
|
||||
},
|
||||
children: (
|
||||
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
|
||||
@ -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,
|
||||
|
@ -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<any> | 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,
|
||||
|
@ -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',
|
||||
|
685
frontend/src/container/APIKeys/APIKeys.styles.scss
Normal file
685
frontend/src/container/APIKeys/APIKeys.styles.scss
Normal file
@ -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);
|
||||
}
|
||||
}
|
99
frontend/src/container/APIKeys/APIKeys.test.tsx
Normal file
99
frontend/src/container/APIKeys/APIKeys.test.tsx
Normal file
@ -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(<APIKeys />);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
864
frontend/src/container/APIKeys/APIKeys.tsx
Normal file
864
frontend/src/container/APIKeys/APIKeys.tsx
Normal file
@ -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<AppState, AppReducer>((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<APIKeyProps | null>();
|
||||
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [dataSource, setDataSource] = useState<APIKeyProps[]>([]);
|
||||
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<HTMLInputElement>): 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<APIKeyProps>['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: (
|
||||
<div className="title-with-action">
|
||||
<div className="api-key-data">
|
||||
<div className="api-key-title">
|
||||
<Typography.Text>{APIKey?.name}</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className="api-key-value">
|
||||
<Typography.Text>
|
||||
{APIKey?.token.substring(0, 2)}********
|
||||
{APIKey?.token.substring(APIKey.token.length - 2).trim()}
|
||||
</Typography.Text>
|
||||
|
||||
<Copy
|
||||
className="copy-key-btn"
|
||||
size={12}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleCopyKey(APIKey.token);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{APIKey.role === USER_ROLES.ADMIN && (
|
||||
<Tooltip title={USER_ROLES.ADMIN}>
|
||||
<Contact2 size={14} color={Color.BG_ROBIN_400} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{APIKey.role === USER_ROLES.EDITOR && (
|
||||
<Tooltip title={USER_ROLES.EDITOR}>
|
||||
<ClipboardEdit size={14} color={Color.BG_ROBIN_400} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{APIKey.role === USER_ROLES.VIEWER && (
|
||||
<Tooltip title={USER_ROLES.VIEWER}>
|
||||
<View size={14} color={Color.BG_ROBIN_400} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!APIKey.role && (
|
||||
<Tooltip title={USER_ROLES.ADMIN}>
|
||||
<Contact2 size={14} color={Color.BG_ROBIN_400} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="action-btn">
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<PenLine size={14} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showEditModal(APIKey);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showDeleteModal(APIKey);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
children: (
|
||||
<div className="api-key-info-container">
|
||||
{APIKey?.createdByUser && (
|
||||
<Row>
|
||||
<Col span={6}> Creator </Col>
|
||||
<Col span={12} className="user-info">
|
||||
<Avatar className="user-avatar" size="small">
|
||||
{APIKey?.createdByUser?.name?.substring(0, 1)}
|
||||
</Avatar>
|
||||
|
||||
<Typography.Text>{APIKey.createdByUser?.name}</Typography.Text>
|
||||
|
||||
<div className="user-email">{APIKey.createdByUser?.email}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row>
|
||||
<Col span={6}> Created on </Col>
|
||||
<Col span={12}>
|
||||
<Typography.Text>{createdOn}</Typography.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
{updatedOn && (
|
||||
<Row>
|
||||
<Col span={6}> Updated on </Col>
|
||||
<Col span={12}>
|
||||
<Typography.Text>{updatedOn}</Typography.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Row>
|
||||
<Col span={6}> Expires on </Col>
|
||||
<Col span={12}>
|
||||
<Typography.Text>{expiresOn}</Typography.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="column-render">
|
||||
<Collapse items={items} />
|
||||
|
||||
<div className="api-key-details">
|
||||
<div className="api-key-last-used-at">
|
||||
<CalendarClock size={14} />
|
||||
Last used <Minus size={12} />
|
||||
<Typography.Text>{formattedDateAndTime}</Typography.Text>
|
||||
</div>
|
||||
|
||||
{!isExpired && expiresIn <= EXPIRATION_WITHIN_SEVEN_DAYS && (
|
||||
<div
|
||||
className={cx(
|
||||
'api-key-expires-in',
|
||||
expiresIn <= 3 ? 'danger' : 'warning',
|
||||
)}
|
||||
>
|
||||
<span className="dot" /> Expires in {expiresIn} Days
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isExpired && (
|
||||
<div className={cx('api-key-expires-in danger')}>
|
||||
<span className="dot" /> Expired
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="api-key-container">
|
||||
<div className="api-key-content">
|
||||
<header>
|
||||
<Typography.Title className="title">API Keys</Typography.Title>
|
||||
<Typography.Text className="subtitle">
|
||||
Create and manage access keys for the SigNoz API
|
||||
</Typography.Text>
|
||||
</header>
|
||||
|
||||
<div className="api-keys-search-add-new">
|
||||
<Input
|
||||
placeholder="Search for keys..."
|
||||
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
|
||||
value={searchValue}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="add-new-api-key-btn"
|
||||
type="primary"
|
||||
onClick={showAddModal}
|
||||
>
|
||||
<Plus size={14} /> New Key
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
loading={isLoading || isRefetching}
|
||||
showHeader={false}
|
||||
pagination={{
|
||||
pageSize: 5,
|
||||
hideOnSinglePage: true,
|
||||
showTotal: (total: number, range: number[]): string =>
|
||||
`${range[0]}-${range[1]} of ${total} API Keys`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Delete Key Modal */}
|
||||
<Modal
|
||||
className="delete-api-key-modal"
|
||||
title={<span className="title">Delete key</span>}
|
||||
open={isDeleteModalOpen}
|
||||
closable
|
||||
afterClose={handleModalClose}
|
||||
onCancel={hideDeleteViewModal}
|
||||
destroyOnClose
|
||||
footer={[
|
||||
<Button
|
||||
key="cancel"
|
||||
onClick={hideDeleteViewModal}
|
||||
className="cancel-btn"
|
||||
icon={<X size={16} />}
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
icon={<Trash2 size={16} />}
|
||||
loading={isDeleteingAPIKey}
|
||||
onClick={onDeleteHandler}
|
||||
className="delete-btn"
|
||||
>
|
||||
Delete key
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Typography.Text className="delete-text">
|
||||
{t('delete_confirm_message', {
|
||||
keyName: activeAPIKey?.name,
|
||||
})}
|
||||
</Typography.Text>
|
||||
</Modal>
|
||||
|
||||
{/* Edit Key Modal */}
|
||||
<Modal
|
||||
className="api-key-modal"
|
||||
title="Edit key"
|
||||
open={isEditModalOpen}
|
||||
key="edit-api-key-modal"
|
||||
afterClose={handleModalClose}
|
||||
// closable
|
||||
onCancel={hideEditViewModal}
|
||||
destroyOnClose
|
||||
footer={[
|
||||
<Button
|
||||
key="cancel"
|
||||
onClick={hideEditViewModal}
|
||||
className="periscope-btn cancel-btn"
|
||||
icon={<X size={16} />}
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
className="periscope-btn primary"
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={isLoadingUpdateAPIKey}
|
||||
icon={<Check size={14} />}
|
||||
onClick={onUpdateApiKey}
|
||||
>
|
||||
Update key
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
name="edit-api-key-form"
|
||||
key={activeAPIKey?.id}
|
||||
form={editForm}
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
initialValues={{
|
||||
name: activeAPIKey?.name,
|
||||
role: activeAPIKey?.role,
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{ required: true }, { type: 'string', min: 6 }]}
|
||||
>
|
||||
<Input placeholder="Enter Key Name" autoFocus />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="role" label="Role">
|
||||
<Flex vertical gap="middle">
|
||||
<Radio.Group
|
||||
buttonStyle="solid"
|
||||
className="api-key-access-role"
|
||||
defaultValue={activeAPIKey?.role}
|
||||
>
|
||||
<Radio.Button value={USER_ROLES.ADMIN} className={cx('tab')}>
|
||||
<div className="role">
|
||||
<Contact2 size={14} /> Admin
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={USER_ROLES.EDITOR} className={cx('tab')}>
|
||||
<div className="role">
|
||||
<ClipboardEdit size={14} /> Editor
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={USER_ROLES.VIEWER} className={cx('tab')}>
|
||||
<div className="role">
|
||||
<Eye size={14} /> Viewer
|
||||
</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* Create New Key Modal */}
|
||||
<Modal
|
||||
className="api-key-modal"
|
||||
title="Create new key"
|
||||
open={isAddModalOpen}
|
||||
key="create-api-key-modal"
|
||||
closable
|
||||
onCancel={hideAddViewModal}
|
||||
destroyOnClose
|
||||
footer={
|
||||
showNewAPIKeyDetails
|
||||
? [
|
||||
<Button
|
||||
key="copy-key-close"
|
||||
className="periscope-btn primary"
|
||||
data-testid="copy-key-close-btn"
|
||||
type="primary"
|
||||
onClick={handleCopyClose}
|
||||
icon={<Check size={12} />}
|
||||
>
|
||||
Copy key and close
|
||||
</Button>,
|
||||
]
|
||||
: [
|
||||
<Button
|
||||
key="cancel"
|
||||
onClick={hideAddViewModal}
|
||||
className="periscope-btn cancel-btn"
|
||||
icon={<X size={16} />}
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
className="periscope-btn primary"
|
||||
test-id="create-new-key"
|
||||
key="submit"
|
||||
type="primary"
|
||||
icon={<Check size={14} />}
|
||||
loading={isLoadingCreateAPIKey}
|
||||
onClick={onCreateAPIKey}
|
||||
>
|
||||
Create new key
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
>
|
||||
{!showNewAPIKeyDetails && (
|
||||
<Form
|
||||
key="createForm"
|
||||
name="create-api-key-form"
|
||||
form={createForm}
|
||||
initialValues={{
|
||||
role: USER_ROLES.ADMIN,
|
||||
expiration: '1',
|
||||
name: '',
|
||||
}}
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{ required: true }, { type: 'string', min: 6 }]}
|
||||
validateTrigger="onFinish"
|
||||
>
|
||||
<Input placeholder="Enter Key Name" autoFocus />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="role" label="Role">
|
||||
<Flex vertical gap="middle">
|
||||
<Radio.Group
|
||||
buttonStyle="solid"
|
||||
className="api-key-access-role"
|
||||
defaultValue={USER_ROLES.ADMIN}
|
||||
>
|
||||
<Radio.Button value={USER_ROLES.ADMIN} className={cx('tab')}>
|
||||
<div className="role" data-testid="create-form-admin-role-btn">
|
||||
<Contact2 size={14} /> Admin
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={USER_ROLES.EDITOR} className="tab">
|
||||
<div className="role" data-testid="create-form-editor-role-btn">
|
||||
<ClipboardEdit size={14} /> Editor
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={USER_ROLES.VIEWER} className="tab">
|
||||
<div className="role" data-testid="create-form-viewer-role-btn">
|
||||
<Eye size={14} /> Viewer
|
||||
</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
<Form.Item name="expiration" label="Expiration">
|
||||
<Select
|
||||
className="expiration-selector"
|
||||
placeholder="Expiration"
|
||||
options={API_KEY_EXPIRY_OPTIONS}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
{showNewAPIKeyDetails && (
|
||||
<div className="api-key-info-container">
|
||||
<Row>
|
||||
<Col span={8}>Key</Col>
|
||||
<Col span={16}>
|
||||
<span className="copyable-text">
|
||||
<Typography.Text>
|
||||
{activeAPIKey?.token.substring(0, 2)}****************
|
||||
{activeAPIKey?.token.substring(activeAPIKey.token.length - 2).trim()}
|
||||
</Typography.Text>
|
||||
|
||||
<Copy
|
||||
className="copy-key-btn"
|
||||
size={12}
|
||||
onClick={(): void => {
|
||||
if (activeAPIKey) {
|
||||
handleCopyKey(activeAPIKey.token);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col span={8}>Name</Col>
|
||||
<Col span={16}>{activeAPIKey?.name}</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col span={8}>Role</Col>
|
||||
<Col span={16}>
|
||||
{activeAPIKey?.role === USER_ROLES.ADMIN && (
|
||||
<div className="role">
|
||||
<Contact2 size={14} /> Admin
|
||||
</div>
|
||||
)}
|
||||
{activeAPIKey?.role === USER_ROLES.EDITOR && (
|
||||
<div className="role">
|
||||
{' '}
|
||||
<ClipboardEdit size={14} /> Editor
|
||||
</div>
|
||||
)}
|
||||
{activeAPIKey?.role === USER_ROLES.VIEWER && (
|
||||
<div className="role">
|
||||
{' '}
|
||||
<View size={14} /> Viewer
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col span={8}>Creator</Col>
|
||||
|
||||
<Col span={16} className="user-info">
|
||||
<Avatar className="user-avatar" size="small">
|
||||
{activeAPIKey?.createdByUser?.name?.substring(0, 1)}
|
||||
</Avatar>
|
||||
|
||||
<Typography.Text>{activeAPIKey?.createdByUser?.name}</Typography.Text>
|
||||
|
||||
<div className="user-email">{activeAPIKey?.createdByUser?.email}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{activeAPIKey?.createdAt && (
|
||||
<Row>
|
||||
<Col span={8}>Created on</Col>
|
||||
<Col span={16}>{getFormattedTime(activeAPIKey?.createdAt)}</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{activeAPIKey?.expiresAt !== 0 && activeAPIKey?.expiresAt && (
|
||||
<Row>
|
||||
<Col span={8}>Expires on</Col>
|
||||
<Col span={16}>{getFormattedTime(activeAPIKey?.expiresAt)}</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{activeAPIKey?.expiresAt === 0 && (
|
||||
<Row>
|
||||
<Col span={8}>Expires on</Col>
|
||||
<Col span={16}> No Expiry </Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default APIKeys;
|
@ -16,16 +16,21 @@ function Controls({
|
||||
handleNavigatePrevious,
|
||||
handleNavigateNext,
|
||||
handleCountItemsPerPageChange,
|
||||
isLogPanel = false,
|
||||
}: ControlsProps): JSX.Element | null {
|
||||
const isNextAndPreviousDisabled = useMemo(
|
||||
() => isLoading || countPerPage < 0 || totalCount === 0,
|
||||
[isLoading, countPerPage, totalCount],
|
||||
);
|
||||
const isPreviousDisabled = useMemo(() => offset <= 0, [offset]);
|
||||
const isNextDisabled = useMemo(() => totalCount < countPerPage, [
|
||||
countPerPage,
|
||||
totalCount,
|
||||
]);
|
||||
const isPreviousDisabled = useMemo(
|
||||
() => (isLogPanel ? false : offset <= 0 || isNextAndPreviousDisabled),
|
||||
[isLogPanel, isNextAndPreviousDisabled, offset],
|
||||
);
|
||||
const isNextDisabled = useMemo(
|
||||
() =>
|
||||
isLogPanel ? false : totalCount < countPerPage || isNextAndPreviousDisabled,
|
||||
[countPerPage, isLogPanel, isNextAndPreviousDisabled, totalCount],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@ -33,7 +38,7 @@ function Controls({
|
||||
loading={isLoading}
|
||||
size="small"
|
||||
type="link"
|
||||
disabled={isPreviousDisabled || isNextAndPreviousDisabled}
|
||||
disabled={isPreviousDisabled}
|
||||
onClick={handleNavigatePrevious}
|
||||
>
|
||||
<LeftOutlined /> Previous
|
||||
@ -42,7 +47,7 @@ function Controls({
|
||||
loading={isLoading}
|
||||
size="small"
|
||||
type="link"
|
||||
disabled={isNextDisabled || isNextAndPreviousDisabled}
|
||||
disabled={isNextDisabled}
|
||||
onClick={handleNavigateNext}
|
||||
>
|
||||
Next <RightOutlined />
|
||||
@ -68,6 +73,7 @@ function Controls({
|
||||
Controls.defaultProps = {
|
||||
offset: 0,
|
||||
perPageOptions: DEFAULT_PER_PAGE_OPTIONS,
|
||||
isLogPanel: false,
|
||||
};
|
||||
|
||||
export interface ControlsProps {
|
||||
@ -79,6 +85,7 @@ export interface ControlsProps {
|
||||
handleNavigatePrevious: () => void;
|
||||
handleNavigateNext: () => void;
|
||||
handleCountItemsPerPageChange: (value: Pagination['limit']) => void;
|
||||
isLogPanel?: boolean;
|
||||
}
|
||||
|
||||
export default memo(Controls);
|
||||
|
@ -50,6 +50,8 @@ function CreateAlertChannels({
|
||||
|
||||
*Summary:* {{ .Annotations.summary }}
|
||||
*Description:* {{ .Annotations.description }}
|
||||
*RelatedLogs:* {{ .Annotations.related_logs }}
|
||||
*RelatedTraces:* {{ .Annotations.related_traces }}
|
||||
|
||||
*Details:*
|
||||
{{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }}
|
||||
|
@ -10,7 +10,6 @@
|
||||
.graph-container {
|
||||
height: calc(60% - 40px);
|
||||
min-height: 300px;
|
||||
border: 1px solid #333;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
@ -18,6 +17,11 @@
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.list-graph-container {
|
||||
height: calc(100% - 40px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
height: calc(100% - 65px);
|
||||
}
|
||||
|
@ -2,9 +2,11 @@ import './WidgetFullView.styles.scss';
|
||||
|
||||
import { SyncOutlined } from '@ant-design/icons';
|
||||
import { Button } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import Spinner from 'components/Spinner';
|
||||
import TimePreference from 'components/TimePreferenceDropDown';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import {
|
||||
timeItems,
|
||||
@ -96,7 +98,7 @@ function FullView({
|
||||
},
|
||||
{
|
||||
queryKey: `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`,
|
||||
enabled: !isDependedDataLoaded,
|
||||
enabled: !isDependedDataLoaded && widget.panelTypes !== PANEL_TYPES.LIST, // Internally both the list view panel has it's own query range api call, so we don't need to call it again
|
||||
},
|
||||
);
|
||||
|
||||
@ -164,6 +166,8 @@ function FullView({
|
||||
parentGraphVisibilityState(graphsVisibilityStates);
|
||||
}, [graphsVisibilityStates, parentGraphVisibilityState]);
|
||||
|
||||
const isListView = widget.panelTypes === PANEL_TYPES.LIST;
|
||||
|
||||
if (response.isFetching) {
|
||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||
}
|
||||
@ -192,14 +196,17 @@ function FullView({
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
isDashboardLocked ? 'graph-container disabled' : 'graph-container'
|
||||
}
|
||||
className={cx('graph-container', {
|
||||
disabled: isDashboardLocked,
|
||||
'list-graph-container': isListView,
|
||||
})}
|
||||
ref={fullViewRef}
|
||||
>
|
||||
{chartOptions && (
|
||||
<GraphContainer
|
||||
style={{ height: '90%' }}
|
||||
style={{
|
||||
height: isListView ? '100%' : '90%',
|
||||
}}
|
||||
isGraphLegendToggleAvailable={canModifyChart}
|
||||
>
|
||||
<GridPanelSwitch
|
||||
@ -214,6 +221,10 @@ function FullView({
|
||||
query={widget.query}
|
||||
ref={fullViewChartRef}
|
||||
thresholds={widget.thresholds}
|
||||
selectedLogFields={widget.selectedLogFields}
|
||||
dataSource={widget.query.builder.queryData[0].dataSource}
|
||||
selectedTracesFields={widget.selectedTracesFields}
|
||||
selectedTime={selectedTime}
|
||||
/>
|
||||
</GraphContainer>
|
||||
)}
|
||||
|
@ -5,6 +5,7 @@ import cx from 'classnames';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@ -47,6 +48,7 @@ function WidgetGraphComponent({
|
||||
onClickHandler,
|
||||
onDragSelect,
|
||||
setGraphVisibility,
|
||||
isFetchingResponse,
|
||||
}: WidgetGraphComponentProps): JSX.Element {
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
@ -222,7 +224,11 @@ function WidgetGraphComponent({
|
||||
});
|
||||
};
|
||||
|
||||
if (queryResponse.isLoading || queryResponse.status === 'idle') {
|
||||
const loadingState =
|
||||
(queryResponse.isLoading || queryResponse.status === 'idle') &&
|
||||
widget.panelTypes !== PANEL_TYPES.LIST;
|
||||
|
||||
if (loadingState) {
|
||||
return (
|
||||
<Skeleton
|
||||
style={{
|
||||
@ -273,6 +279,7 @@ function WidgetGraphComponent({
|
||||
onCancel={onToggleModelHandler}
|
||||
width="85%"
|
||||
destroyOnClose
|
||||
className="widget-full-view"
|
||||
>
|
||||
<FullView
|
||||
name={`${name}expanded`}
|
||||
@ -300,10 +307,11 @@ function WidgetGraphComponent({
|
||||
threshold={threshold}
|
||||
headerMenuList={headerMenuList}
|
||||
isWarning={isWarning}
|
||||
isFetchingResponse={isFetchingResponse}
|
||||
/>
|
||||
</div>
|
||||
{queryResponse.isLoading && <Skeleton />}
|
||||
{queryResponse.isSuccess && (
|
||||
{(queryResponse.isSuccess || widget.panelTypes === PANEL_TYPES.LIST) && (
|
||||
<div
|
||||
className={cx('widget-graph-container', widget.panelTypes)}
|
||||
ref={graphRef}
|
||||
@ -319,6 +327,9 @@ function WidgetGraphComponent({
|
||||
panelData={queryResponse.data?.payload?.data.newResult.data.result || []}
|
||||
query={widget.query}
|
||||
thresholds={widget.thresholds}
|
||||
selectedLogFields={widget.selectedLogFields}
|
||||
dataSource={widget.query.builder?.queryData[0]?.dataSource}
|
||||
selectedTracesFields={widget.selectedTracesFields}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -116,6 +116,12 @@ function GridCardGraph({
|
||||
const isEmptyWidget =
|
||||
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
|
||||
|
||||
const queryEnabledCondition =
|
||||
isVisible &&
|
||||
!isEmptyWidget &&
|
||||
isQueryEnabled &&
|
||||
widget.panelTypes !== PANEL_TYPES.LIST;
|
||||
|
||||
const queryResponse = useGetQueryRange(
|
||||
{
|
||||
selectedTime: widget?.timePreferance,
|
||||
@ -135,7 +141,7 @@ function GridCardGraph({
|
||||
widget.timePreferance,
|
||||
],
|
||||
keepPreviousData: true,
|
||||
enabled: isVisible && !isEmptyWidget && isQueryEnabled,
|
||||
enabled: queryEnabledCondition,
|
||||
refetchOnMount: false,
|
||||
onError: (error) => {
|
||||
setErrorMessage(error.message);
|
||||
@ -159,7 +165,8 @@ function GridCardGraph({
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const menuList =
|
||||
widget.panelTypes === PANEL_TYPES.TABLE
|
||||
widget.panelTypes === PANEL_TYPES.TABLE ||
|
||||
widget.panelTypes === PANEL_TYPES.LIST
|
||||
? headerMenuList.filter((menu) => menu !== MenuItemKeys.CreateAlerts)
|
||||
: headerMenuList;
|
||||
|
||||
@ -222,6 +229,7 @@ function GridCardGraph({
|
||||
onClickHandler={onClickHandler}
|
||||
graphVisibiltyState={graphVisibility}
|
||||
setGraphVisibility={setGraphVisibility}
|
||||
isFetchingResponse={queryResponse.isFetching}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -30,6 +30,7 @@ export interface WidgetGraphComponentProps extends UplotProps {
|
||||
isWarning: boolean;
|
||||
graphVisibiltyState: boolean[];
|
||||
setGraphVisibility: Dispatch<SetStateAction<boolean[]>>;
|
||||
isFetchingResponse: boolean;
|
||||
}
|
||||
|
||||
export interface GridCardGraphProps {
|
||||
|
@ -16,8 +16,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
.widget-full-view {
|
||||
.ant-modal-content {
|
||||
background-color: var(--bg-ink-400);
|
||||
|
||||
.ant-modal-header {
|
||||
background-color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.fullscreen-grid-container {
|
||||
background-color: rgb(250, 250, 250);
|
||||
}
|
||||
|
||||
.widget-full-view {
|
||||
.ant-modal-content {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
|
@ -45,6 +45,7 @@ interface IWidgetHeaderProps {
|
||||
threshold?: ReactNode;
|
||||
headerMenuList?: MenuItemKeys[];
|
||||
isWarning: boolean;
|
||||
isFetchingResponse: boolean;
|
||||
}
|
||||
|
||||
function WidgetHeader({
|
||||
@ -59,6 +60,7 @@ function WidgetHeader({
|
||||
threshold,
|
||||
headerMenuList,
|
||||
isWarning,
|
||||
isFetchingResponse,
|
||||
}: IWidgetHeaderProps): JSX.Element | null {
|
||||
const onEditHandler = useCallback((): void => {
|
||||
const widgetId = widget.id;
|
||||
@ -170,7 +172,7 @@ function WidgetHeader({
|
||||
</Typography.Text>
|
||||
<div className="widget-header-actions">
|
||||
<div className="widget-api-actions">{threshold}</div>
|
||||
{queryResponse.isFetching && !queryResponse.isError && (
|
||||
{isFetchingResponse && !queryResponse.isError && (
|
||||
<Spinner style={{ paddingRight: '0.25rem' }} />
|
||||
)}
|
||||
{queryResponse.isError && (
|
||||
|
@ -17,7 +17,7 @@ export const Card = styled(CardComponent)<CardProps>`
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
height: calc(100% - 40px);
|
||||
height: calc(100% - 30px);
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { PANEL_TYPES_COMPONENT_MAP } from 'constants/panelTypes';
|
||||
import { getComponentForPanelType } from 'constants/panelTypes';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
|
||||
import { FC, forwardRef, memo, useMemo } from 'react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { GridPanelSwitchProps, PropsTypePropsMap } from './types';
|
||||
|
||||
@ -11,7 +12,19 @@ const GridPanelSwitch = forwardRef<
|
||||
GridPanelSwitchProps
|
||||
>(
|
||||
(
|
||||
{ panelType, data, yAxisUnit, panelData, query, options, thresholds },
|
||||
{
|
||||
panelType,
|
||||
data,
|
||||
yAxisUnit,
|
||||
panelData,
|
||||
query,
|
||||
options,
|
||||
thresholds,
|
||||
selectedLogFields,
|
||||
selectedTracesFields,
|
||||
dataSource,
|
||||
selectedTime,
|
||||
},
|
||||
ref,
|
||||
): JSX.Element | null => {
|
||||
const currentProps: PropsTypePropsMap = useMemo(() => {
|
||||
@ -32,15 +45,38 @@ const GridPanelSwitch = forwardRef<
|
||||
query,
|
||||
thresholds,
|
||||
},
|
||||
[PANEL_TYPES.LIST]: null,
|
||||
[PANEL_TYPES.LIST]:
|
||||
dataSource === DataSource.LOGS
|
||||
? {
|
||||
selectedLogsFields: selectedLogFields || [],
|
||||
query,
|
||||
selectedTime,
|
||||
}
|
||||
: {
|
||||
selectedTracesFields: selectedTracesFields || [],
|
||||
query,
|
||||
selectedTime,
|
||||
},
|
||||
[PANEL_TYPES.TRACE]: null,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
||||
};
|
||||
|
||||
return result;
|
||||
}, [data, options, ref, yAxisUnit, thresholds, panelData, query]);
|
||||
}, [
|
||||
data,
|
||||
options,
|
||||
ref,
|
||||
yAxisUnit,
|
||||
thresholds,
|
||||
panelData,
|
||||
query,
|
||||
dataSource,
|
||||
selectedLogFields,
|
||||
selectedTime,
|
||||
selectedTracesFields,
|
||||
]);
|
||||
|
||||
const Component = PANEL_TYPES_COMPONENT_MAP[panelType] as FC<
|
||||
const Component = getComponentForPanelType(panelType, dataSource) as FC<
|
||||
PropsTypePropsMap[typeof panelType]
|
||||
>;
|
||||
const componentProps = useMemo(() => currentProps[panelType], [
|
||||
|
@ -2,11 +2,15 @@ import { StaticLineProps, ToggleGraphProps } from 'components/Graph/types';
|
||||
import { UplotProps } from 'components/Uplot/Uplot';
|
||||
import { GridTableComponentProps } from 'container/GridTableComponent/types';
|
||||
import { GridValueComponentProps } from 'container/GridValueComponent/types';
|
||||
import { LogsPanelComponentProps } from 'container/LogsPanelTable/LogsPanelComponent';
|
||||
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { TracesTableComponentProps } from 'container/TracesTableComponent/TracesTableComponent';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { ForwardedRef } from 'react';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { PANEL_TYPES } from '../../constants/queryBuilder';
|
||||
@ -23,6 +27,10 @@ export type GridPanelSwitchProps = {
|
||||
panelData: QueryDataV3[];
|
||||
query: Query;
|
||||
thresholds?: Widgets['thresholds'];
|
||||
dataSource?: DataSource;
|
||||
selectedLogFields?: Widgets['selectedLogFields'];
|
||||
selectedTracesFields?: Widgets['selectedTracesFields'];
|
||||
selectedTime?: timePreferance;
|
||||
};
|
||||
|
||||
export type PropsTypePropsMap = {
|
||||
@ -32,6 +40,6 @@ export type PropsTypePropsMap = {
|
||||
[PANEL_TYPES.VALUE]: GridValueComponentProps;
|
||||
[PANEL_TYPES.TABLE]: GridTableComponentProps;
|
||||
[PANEL_TYPES.TRACE]: null;
|
||||
[PANEL_TYPES.LIST]: null;
|
||||
[PANEL_TYPES.LIST]: LogsPanelComponentProps | TracesTableComponentProps;
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: null;
|
||||
};
|
||||
|
@ -22,6 +22,7 @@ import TableView from './TableView';
|
||||
|
||||
interface OverviewProps {
|
||||
logData: ILog;
|
||||
isListViewPanel?: boolean;
|
||||
}
|
||||
|
||||
type Props = OverviewProps &
|
||||
@ -32,6 +33,7 @@ function Overview({
|
||||
logData,
|
||||
onAddToQuery,
|
||||
onClickActionItem,
|
||||
isListViewPanel = false,
|
||||
}: Props): JSX.Element {
|
||||
const [isWrapWord, setIsWrapWord] = useState<boolean>(false);
|
||||
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
|
||||
@ -199,6 +201,7 @@ function Overview({
|
||||
onAddToQuery={onAddToQuery}
|
||||
fieldSearchInput={fieldSearchInput}
|
||||
onClickActionItem={onClickActionItem}
|
||||
isListViewPanel={isListViewPanel}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
@ -210,4 +213,8 @@ function Overview({
|
||||
);
|
||||
}
|
||||
|
||||
Overview.defaultProps = {
|
||||
isListViewPanel: false,
|
||||
};
|
||||
|
||||
export default Overview;
|
||||
|
@ -40,6 +40,7 @@ const RESTRICTED_FIELDS = ['timestamp'];
|
||||
interface TableViewProps {
|
||||
logData: ILog;
|
||||
fieldSearchInput: string;
|
||||
isListViewPanel?: boolean;
|
||||
}
|
||||
|
||||
type Props = TableViewProps &
|
||||
@ -51,6 +52,7 @@ function TableView({
|
||||
fieldSearchInput,
|
||||
onAddToQuery,
|
||||
onClickActionItem,
|
||||
isListViewPanel = false,
|
||||
}: Props): JSX.Element | null {
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
|
||||
@ -100,7 +102,10 @@ function TableView({
|
||||
value: JSON.stringify(flattenLogData[key]),
|
||||
}));
|
||||
|
||||
const onTraceHandler = (record: DataType) => (): void => {
|
||||
const onTraceHandler = (
|
||||
record: DataType,
|
||||
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
) => (): void => {
|
||||
if (flattenLogData === null) return;
|
||||
|
||||
const traceId = flattenLogData[record.field];
|
||||
@ -119,7 +124,12 @@ function TableView({
|
||||
|
||||
const route = spanId ? `${basePath}?spanId=${spanId}` : basePath;
|
||||
|
||||
history.push(route);
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
// open the trace in new tab
|
||||
window.open(route, '_blank');
|
||||
} else {
|
||||
history.push(route);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -148,17 +158,20 @@ function TableView({
|
||||
|
||||
{traceId && (
|
||||
<Tooltip title="Inspect in Trace">
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
role="presentation"
|
||||
onClick={onTraceHandler(record)}
|
||||
<Button
|
||||
className="periscope-btn"
|
||||
onClick={(
|
||||
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
): void => {
|
||||
onTraceHandler(record, event);
|
||||
}}
|
||||
>
|
||||
<LinkOutlined
|
||||
style={{
|
||||
width: '15px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
@ -207,38 +220,45 @@ function TableView({
|
||||
{removeEscapeCharacters(fieldData.value)}
|
||||
</span>
|
||||
</CopyClipboardHOC>
|
||||
<span className="action-btn">
|
||||
<Tooltip title="Filter for value">
|
||||
<Button
|
||||
className="filter-btn periscope-btn"
|
||||
icon={
|
||||
isfilterInLoading ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<ArrowDownToDot size={14} style={{ transform: 'rotate(90deg)' }} />
|
||||
)
|
||||
}
|
||||
onClick={onClickHandler(OPERATORS.IN, fieldFilterKey, fieldData.value)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Filter out value">
|
||||
<Button
|
||||
className="filter-btn periscope-btn"
|
||||
icon={
|
||||
isfilterOutLoading ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<ArrowUpFromDot size={14} style={{ transform: 'rotate(90deg)' }} />
|
||||
)
|
||||
}
|
||||
onClick={onClickHandler(
|
||||
OPERATORS.NIN,
|
||||
fieldFilterKey,
|
||||
fieldData.value,
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
||||
{!isListViewPanel && (
|
||||
<span className="action-btn">
|
||||
<Tooltip title="Filter for value">
|
||||
<Button
|
||||
className="filter-btn periscope-btn"
|
||||
icon={
|
||||
isfilterInLoading ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<ArrowDownToDot size={14} style={{ transform: 'rotate(90deg)' }} />
|
||||
)
|
||||
}
|
||||
onClick={onClickHandler(
|
||||
OPERATORS.IN,
|
||||
fieldFilterKey,
|
||||
fieldData.value,
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Filter out value">
|
||||
<Button
|
||||
className="filter-btn periscope-btn"
|
||||
icon={
|
||||
isfilterOutLoading ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<ArrowUpFromDot size={14} style={{ transform: 'rotate(90deg)' }} />
|
||||
)
|
||||
}
|
||||
onClick={onClickHandler(
|
||||
OPERATORS.NIN,
|
||||
fieldFilterKey,
|
||||
fieldData.value,
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@ -257,6 +277,10 @@ function TableView({
|
||||
);
|
||||
}
|
||||
|
||||
TableView.defaultProps = {
|
||||
isListViewPanel: false,
|
||||
};
|
||||
|
||||
interface DataType {
|
||||
key: string;
|
||||
field: string;
|
||||
|
@ -3,34 +3,30 @@
|
||||
|
||||
.show-more-button {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&.up {
|
||||
top: 0;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
&.down {
|
||||
bottom: 0;
|
||||
bottom: -1px;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.virtuoso-list {
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
height: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.logs-context-list-asc {
|
||||
.virtuoso-list {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&.logs-context-list-desc {
|
||||
.virtuoso-list {
|
||||
padding-bottom: 16px;
|
||||
width: 0.3rem;
|
||||
height: 0.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +1,31 @@
|
||||
.show-more-button {
|
||||
background-color: var(--bg-slate-400);
|
||||
color: var(--bg-vanilla-100);
|
||||
display: flex;
|
||||
padding: 4px 8px;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
border: none;
|
||||
margin: 0;
|
||||
background-color: var(--bg-slate-400);
|
||||
color: var(--bg-vanilla-100);
|
||||
display: flex;
|
||||
padding: 4px 8px;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.show-more-button {
|
||||
&.disabled {
|
||||
background-color: var(--bg-slate-200);
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
&.disabled {
|
||||
background-color: var(--bg-slate-200);
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.show-more-button {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
color: var(--bg-slate-400);
|
||||
}
|
||||
.show-more-button {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
color: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
.show-more-button {
|
||||
&.disabled {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
.show-more-button {
|
||||
&.disabled {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ function ShowButton({
|
||||
onClick={onClick}
|
||||
icon={getIcons()}
|
||||
className={cx(
|
||||
'show-more-button',
|
||||
'show-more-button periscope-btn',
|
||||
order === ORDERBY_FILTERS.ASC ? 'up' : 'down',
|
||||
isDisabled && 'disabled',
|
||||
)}
|
||||
|
@ -1,9 +1,20 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import './LogsError.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import history from 'lib/history';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
export default function LogsError(): JSX.Element {
|
||||
const handleContactSupport = (): void => {
|
||||
if (isCloudUser()) {
|
||||
history.push('/support');
|
||||
} else {
|
||||
window.open('https://signoz.io/slack', '_blank');
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="logs-error-container">
|
||||
<div className="logs-error-content">
|
||||
@ -16,10 +27,12 @@ export default function LogsError(): JSX.Element {
|
||||
<span className="aww-snap">Aw snap :/ </span> Something went wrong. Please
|
||||
try again or contact support.
|
||||
</Typography.Text>
|
||||
<section className="contact-support">
|
||||
|
||||
<div className="contact-support" onClick={handleContactSupport}>
|
||||
<Typography.Link className="text">Contact Support </Typography.Link>
|
||||
|
||||
<ArrowRight size={14} />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -68,6 +68,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
activeLog,
|
||||
activeContextLog,
|
||||
});
|
||||
|
||||
const { draggedColumns, onDragColumns } = useDragColumns<
|
||||
Record<string, unknown>
|
||||
>(LOCALSTORAGE.LOGS_LIST_COLUMNS);
|
||||
|
@ -173,7 +173,9 @@ function LogsExplorerList({
|
||||
|
||||
{!isLoading && !isError && logs.length > 0 && (
|
||||
<>
|
||||
<InfinityWrapperStyled>{renderContent}</InfinityWrapperStyled>
|
||||
<InfinityWrapperStyled data-testid="logs-list-virtuoso">
|
||||
{renderContent}
|
||||
</InfinityWrapperStyled>
|
||||
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
|
@ -395,7 +395,7 @@ function LogsExplorerViews({
|
||||
handleSetConfig(defaultTo(panelTypes, PANEL_TYPES.LIST), DataSource.LOGS);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [handleSetConfig, panelTypes]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentParams = data?.params as Omit<LogTimeRange, 'pageSize'>;
|
||||
@ -539,6 +539,7 @@ function LogsExplorerViews({
|
||||
(isMultipleQueries || isGroupByExist) && selectedView !== 'search'
|
||||
}
|
||||
onClick={(): void => handleModeChange(PANEL_TYPES.LIST)}
|
||||
data-testid="logs-list-view"
|
||||
>
|
||||
List view
|
||||
</Button>
|
||||
@ -551,6 +552,7 @@ function LogsExplorerViews({
|
||||
: 'tab'
|
||||
}
|
||||
onClick={(): void => handleModeChange(PANEL_TYPES.TIME_SERIES)}
|
||||
data-testid="time-series-view"
|
||||
>
|
||||
Time series
|
||||
</Button>
|
||||
@ -561,6 +563,7 @@ function LogsExplorerViews({
|
||||
selectedPanelType === PANEL_TYPES.TABLE ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
onClick={(): void => handleModeChange(PANEL_TYPES.TABLE)}
|
||||
data-testid="table-view"
|
||||
>
|
||||
Table
|
||||
</Button>
|
||||
|
@ -0,0 +1,80 @@
|
||||
.logs-table {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.resize-table {
|
||||
height: calc(92% - 5px);
|
||||
overflow: scroll;
|
||||
|
||||
.ant-table-wrapper .ant-table-tbody >tr >td {
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
font-family: Inter;
|
||||
.ant-typography {
|
||||
background-color: transparent;
|
||||
color: var(--bg-vanilla-100);
|
||||
margin-bottom: 0;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
font-family: Inter;
|
||||
}
|
||||
padding: 14px 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-header {
|
||||
border-bottom: 0.5px solid var(--bg-slate-400);
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-thead > tr > th {
|
||||
font-family: Inter;
|
||||
color: var(--bg-vanilla-100);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-thead > tr > th::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.controller {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.logs-table {
|
||||
.resize-table {
|
||||
.ant-table-wrapper .ant-table-tbody >tr >td {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
.ant-typography {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
.ant-table-wrapper .ant-table-thead > tr > th {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-header {
|
||||
border-bottom: 0.5px solid var(--bg-vanilla-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
311
frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx
Normal file
311
frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx
Normal file
@ -0,0 +1,311 @@
|
||||
import './LogsPanelComponent.styles.scss';
|
||||
|
||||
import { Table } from 'antd';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { OPERATORS, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import Controls from 'container/Controls';
|
||||
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
|
||||
import { tableStyles } from 'container/TracesExplorer/ListView/styles';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import { useLogsData } from 'hooks/useLogsData';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
HTMLAttributes,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { getLogPanelColumnsList } from './utils';
|
||||
|
||||
function LogsPanelComponent({
|
||||
selectedLogsFields,
|
||||
query,
|
||||
selectedTime,
|
||||
}: LogsPanelComponentProps): JSX.Element {
|
||||
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const [pagination, setPagination] = useState<Pagination>({
|
||||
offset: 0,
|
||||
limit: query.builder.queryData[0].limit || 0,
|
||||
});
|
||||
|
||||
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
|
||||
const updatedQuery = { ...query };
|
||||
updatedQuery.builder.queryData[0].pageSize = 10;
|
||||
return {
|
||||
query: updatedQuery,
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
tableParams: {
|
||||
pagination,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setRequestData({
|
||||
...requestData,
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
tableParams: {
|
||||
pagination,
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pagination]);
|
||||
|
||||
const [pageSize, setPageSize] = useState<number>(10);
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
const handleChangePageSize = (value: number): void => {
|
||||
setPagination({
|
||||
...pagination,
|
||||
limit: 0,
|
||||
offset: value,
|
||||
});
|
||||
setPageSize(value);
|
||||
const newQueryData = { ...requestData.query };
|
||||
newQueryData.builder.queryData[0].pageSize = value;
|
||||
const newRequestData = {
|
||||
...requestData,
|
||||
query: newQueryData,
|
||||
tableParams: {
|
||||
pagination,
|
||||
},
|
||||
};
|
||||
setRequestData(newRequestData);
|
||||
};
|
||||
|
||||
const { data, isFetching, isError } = useGetQueryRange(
|
||||
{
|
||||
...requestData,
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
selectedTime: selectedTime?.enum || 'GLOBAL_TIME',
|
||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
requestData,
|
||||
pagination,
|
||||
selectedDashboard?.data.variables,
|
||||
],
|
||||
enabled: !!requestData.query && !!selectedLogsFields?.length,
|
||||
},
|
||||
);
|
||||
|
||||
const columns = getLogPanelColumnsList(selectedLogsFields);
|
||||
|
||||
const dataLength =
|
||||
data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
|
||||
const totalCount = useMemo(() => dataLength || 0, [dataLength]);
|
||||
|
||||
const [firstLog, setFirstLog] = useState<ILog>();
|
||||
const [lastLog, setLastLog] = useState<ILog>();
|
||||
|
||||
const { logs } = useLogsData({
|
||||
result: data?.payload.data.newResult.data.result,
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
stagedQuery: query,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (logs.length) {
|
||||
setFirstLog(logs[0]);
|
||||
setLastLog(logs[logs.length - 1]);
|
||||
}
|
||||
}, [logs]);
|
||||
|
||||
const flattenLogData = useMemo(
|
||||
() => logs.map((log) => FlatLogData(log) as RowData),
|
||||
[logs],
|
||||
);
|
||||
|
||||
const {
|
||||
activeLog,
|
||||
onSetActiveLog,
|
||||
onClearActiveLog,
|
||||
onAddToQuery,
|
||||
} = useActiveLog();
|
||||
|
||||
const handleRow = useCallback(
|
||||
(record: RowData): HTMLAttributes<RowData> => ({
|
||||
onClick: (): void => {
|
||||
const log = logs.find((item) => item.id === record.id);
|
||||
if (log) onSetActiveLog(log);
|
||||
},
|
||||
}),
|
||||
[logs, onSetActiveLog],
|
||||
);
|
||||
|
||||
const isOrderByTimeStamp =
|
||||
query.builder.queryData[0].orderBy.length > 0 &&
|
||||
query.builder.queryData[0].orderBy[0].columnName === 'timestamp';
|
||||
|
||||
const handlePreviousPagination = (): void => {
|
||||
if (isOrderByTimeStamp) {
|
||||
setRequestData({
|
||||
...requestData,
|
||||
query: {
|
||||
...requestData.query,
|
||||
builder: {
|
||||
...requestData.query.builder,
|
||||
queryData: [
|
||||
{
|
||||
...requestData.query.builder.queryData[0],
|
||||
filters: {
|
||||
...requestData.query.builder.queryData[0].filters,
|
||||
items: [
|
||||
{
|
||||
id: uuid(),
|
||||
key: {
|
||||
key: 'id',
|
||||
type: '',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
op: OPERATORS['>'],
|
||||
value: firstLog?.id || '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
setPagination({
|
||||
...pagination,
|
||||
limit: 0,
|
||||
offset: pagination.offset - pageSize,
|
||||
});
|
||||
};
|
||||
|
||||
const handleNextPagination = (): void => {
|
||||
if (isOrderByTimeStamp) {
|
||||
setRequestData({
|
||||
...requestData,
|
||||
query: {
|
||||
...requestData.query,
|
||||
builder: {
|
||||
...requestData.query.builder,
|
||||
queryData: [
|
||||
{
|
||||
...requestData.query.builder.queryData[0],
|
||||
filters: {
|
||||
...requestData.query.builder.queryData[0].filters,
|
||||
items: [
|
||||
{
|
||||
id: uuid(),
|
||||
key: {
|
||||
key: 'id',
|
||||
type: '',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
op: OPERATORS['<'],
|
||||
value: lastLog?.id || '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
setPagination({
|
||||
...pagination,
|
||||
limit: 0,
|
||||
offset: pagination.offset + pageSize,
|
||||
});
|
||||
};
|
||||
|
||||
if (isError) {
|
||||
return <div>{SOMETHING_WENT_WRONG}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="logs-table">
|
||||
<div className="resize-table">
|
||||
<Table
|
||||
pagination={false}
|
||||
tableLayout="fixed"
|
||||
scroll={{ x: `calc(50vw - 10px)` }}
|
||||
sticky
|
||||
loading={isFetching}
|
||||
style={tableStyles}
|
||||
dataSource={flattenLogData}
|
||||
columns={columns}
|
||||
onRow={handleRow}
|
||||
/>
|
||||
</div>
|
||||
{!query.builder.queryData[0].limit && (
|
||||
<div className="controller">
|
||||
<Controls
|
||||
totalCount={totalCount}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
isLoading={isFetching}
|
||||
offset={pagination.offset}
|
||||
countPerPage={pageSize}
|
||||
handleNavigatePrevious={handlePreviousPagination}
|
||||
handleNavigateNext={handleNextPagination}
|
||||
handleCountItemsPerPageChange={handleChangePageSize}
|
||||
isLogPanel={isOrderByTimeStamp}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
isListViewPanel
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export type LogsPanelComponentProps = {
|
||||
selectedLogsFields: Widgets['selectedLogFields'];
|
||||
query: Query;
|
||||
selectedTime?: timePreferance;
|
||||
};
|
||||
|
||||
LogsPanelComponent.defaultProps = {
|
||||
selectedTime: undefined,
|
||||
};
|
||||
|
||||
export default LogsPanelComponent;
|
38
frontend/src/container/LogsPanelTable/utils.tsx
Normal file
38
frontend/src/container/LogsPanelTable/utils.tsx
Normal file
@ -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<RowData> => {
|
||||
const initialColumns: ColumnsType<RowData> = [];
|
||||
|
||||
const columns: ColumnsType<RowData> =
|
||||
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 (
|
||||
<Typography.Paragraph ellipsis={{ rows: 1 }} data-testid={name}>
|
||||
{value}
|
||||
</Typography.Paragraph>
|
||||
);
|
||||
}
|
||||
|
||||
return <Typography.Text data-testid={name}>{value}</Typography.Text>;
|
||||
},
|
||||
responsive: ['md'],
|
||||
};
|
||||
}) || [];
|
||||
|
||||
return [...initialColumns, ...columns];
|
||||
};
|
@ -22,4 +22,6 @@ export const getWidgetQueryBuilder = ({
|
||||
yAxisUnit,
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
selectedLogFields: [],
|
||||
selectedTracesFields: [],
|
||||
});
|
||||
|
@ -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[],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
@ -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: () => {
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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<string>('');
|
||||
const [open, setOpen] = useState<boolean>(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<HTMLInputElement>): void => {
|
||||
setSearchText(e.target.value);
|
||||
};
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: 'search',
|
||||
label: (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
className="explorer-columns-search"
|
||||
value={searchText}
|
||||
onChange={handleSearchChange}
|
||||
prefix={<Search size={16} style={{ padding: '6px' }} />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'columns',
|
||||
label: (
|
||||
<div className="attribute-columns">
|
||||
{data?.payload?.attributeKeys
|
||||
?.filter((attributeKey) =>
|
||||
attributeKey.key.toLowerCase().includes(searchText.toLowerCase()),
|
||||
)
|
||||
?.map((attributeKey) => (
|
||||
<Checkbox
|
||||
checked={isAttributeKeySelected(attributeKey.key)}
|
||||
onChange={(): void => handleCheckboxChange(attributeKey.key)}
|
||||
style={{ padding: 0 }}
|
||||
key={attributeKey.key}
|
||||
>
|
||||
{attributeKey.key}
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
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 <Spinner size="large" tip="Loading..." height="4vh" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="explorer-columns-renderer">
|
||||
<div className="title">
|
||||
<Typography.Text>Columns</Typography.Text>
|
||||
{isError && (
|
||||
<Tooltip title={SOMETHING_WENT_WRONG}>
|
||||
<AlertCircle size={16} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<Divider />
|
||||
{!isError && (
|
||||
<div className="explorer-columns-contents">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="drag-drop-list" direction="horizontal">
|
||||
{(provided): JSX.Element => (
|
||||
<div
|
||||
className="explorer-columns"
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{initialDataSource === DataSource.LOGS &&
|
||||
selectedLogFields &&
|
||||
selectedLogFields.map((field, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Draggable key={index} draggableId={index.toString()} index={index}>
|
||||
{(dragProvided): JSX.Element => (
|
||||
<div
|
||||
className="explorer-column-card"
|
||||
ref={dragProvided.innerRef}
|
||||
{...dragProvided.draggableProps}
|
||||
{...dragProvided.dragHandleProps}
|
||||
>
|
||||
<div className="explorer-column-title">
|
||||
<GripVertical size={12} color="#5A5A5A" />
|
||||
{field.name}
|
||||
</div>
|
||||
<Trash2
|
||||
size={12}
|
||||
color="red"
|
||||
onClick={(): void => removeSelectedLogField(field.name)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{initialDataSource === DataSource.TRACES &&
|
||||
selectedTracesFields &&
|
||||
selectedTracesFields.map((field, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Draggable key={index} draggableId={index.toString()} index={index}>
|
||||
{(dragProvided): JSX.Element => (
|
||||
<div
|
||||
className="explorer-column-card"
|
||||
ref={dragProvided.innerRef}
|
||||
{...dragProvided.draggableProps}
|
||||
{...dragProvided.dragHandleProps}
|
||||
>
|
||||
<div className="explorer-column-title">
|
||||
<GripVertical size={12} color="#5A5A5A" />
|
||||
{field.key}
|
||||
</div>
|
||||
<Trash2
|
||||
size={12}
|
||||
color="red"
|
||||
onClick={(): void => removeSelectedLogField(field.key)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<div>
|
||||
<Dropdown
|
||||
menu={{ items }}
|
||||
arrow
|
||||
placement="top"
|
||||
open={open}
|
||||
overlayClassName="explorer-columns-dropdown"
|
||||
>
|
||||
<Button
|
||||
className="action-btn"
|
||||
icon={
|
||||
<PlusCircle
|
||||
size={16}
|
||||
color={isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100}
|
||||
/>
|
||||
}
|
||||
onClick={toggleDropdown}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExplorerColumnsRenderer;
|
@ -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<AppState, GlobalReducer>(
|
||||
@ -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: (
|
||||
<Tooltip title="Query Builder">
|
||||
<Button className="nav-btns">
|
||||
<Atom size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
),
|
||||
tab: <Typography>Query Builder</Typography>,
|
||||
children: (
|
||||
<QueryBuilder
|
||||
panelType={PANEL_TYPES.LIST}
|
||||
filterConfigs={filterConfigs}
|
||||
isListViewPanel
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: EQueryType.QUERY_BUILDER,
|
||||
@ -180,8 +201,12 @@ function QuerySection({
|
||||
<Tabs
|
||||
type="card"
|
||||
style={{ width: '100%' }}
|
||||
defaultActiveKey={currentTab}
|
||||
activeKey={currentTab}
|
||||
defaultActiveKey={
|
||||
selectedGraph !== PANEL_TYPES.EMPTY_WIDGET
|
||||
? currentQuery.queryType
|
||||
: currentQuery.builder.queryData[0].dataSource
|
||||
}
|
||||
activeKey={currentQuery.queryType}
|
||||
onChange={handleQueryCategoryChange}
|
||||
tabBarExtraContent={
|
||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
@ -197,7 +222,7 @@ function QuerySection({
|
||||
</Button>
|
||||
</span>
|
||||
}
|
||||
items={items}
|
||||
items={selectedGraph === PANEL_TYPES.LIST ? listItems : items}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 <Spinner size="large" tip="Loading..." />;
|
||||
}
|
||||
if (getWidgetQueryRange.data?.payload.data.result.length === 0) {
|
||||
|
||||
if (
|
||||
selectedGraph !== PANEL_TYPES.LIST &&
|
||||
getWidgetQueryRange.data?.payload.data.result.length === 0
|
||||
) {
|
||||
return (
|
||||
<NotFoundContainer>
|
||||
<Typography>No Data</Typography>
|
||||
</NotFoundContainer>
|
||||
);
|
||||
}
|
||||
if (
|
||||
selectedGraph === PANEL_TYPES.LIST &&
|
||||
getWidgetQueryRange.data?.payload.data.newResult.data.result.length === 0
|
||||
) {
|
||||
return (
|
||||
<NotFoundContainer>
|
||||
<Typography>No Data</Typography>
|
||||
@ -63,6 +80,9 @@ function WidgetGraphContainer({
|
||||
fillSpans={fillSpans}
|
||||
softMax={softMax}
|
||||
softMin={softMin}
|
||||
selectedLogFields={selectedLogFields}
|
||||
selectedTracesFields={selectedTracesFields}
|
||||
selectedTime={selectedTime}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -172,6 +180,9 @@ interface WidgetGraphProps {
|
||||
>;
|
||||
softMax: number | null;
|
||||
softMin: number | null;
|
||||
selectedLogFields: Widgets['selectedLogFields'];
|
||||
selectedTracesFields: Widgets['selectedTracesFields'];
|
||||
selectedTime: timePreferance;
|
||||
}
|
||||
|
||||
export default WidgetGraph;
|
||||
|
@ -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}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
@ -13,8 +13,10 @@ export const Container = styled(Card)<Props>`
|
||||
|
||||
.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;
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
<QueryContainer>
|
||||
<QuerySection selectedTime={selectedTime} selectedGraph={selectedGraph} />
|
||||
{selectedGraph === PANEL_TYPES.LIST && (
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={selectedLogFields}
|
||||
setSelectedLogFields={setSelectedLogFields}
|
||||
selectedTracesFields={selectedTracesFields}
|
||||
setSelectedTracesFields={setSelectedTracesFields}
|
||||
/>
|
||||
)}
|
||||
</QueryContainer>
|
||||
</>
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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({
|
||||
}
|
||||
/>
|
||||
|
||||
<Space style={{ marginTop: 10 }} direction="vertical">
|
||||
<Typography>Fill gaps</Typography>
|
||||
{allowFillSpans && (
|
||||
<Space style={{ marginTop: 10 }} direction="vertical">
|
||||
<Typography>Fill gaps</Typography>
|
||||
|
||||
<Switch
|
||||
checked={isFillSpans}
|
||||
onChange={(checked): void => setIsFillSpans(checked)}
|
||||
/>
|
||||
</Space>
|
||||
<Switch
|
||||
checked={isFillSpans}
|
||||
onChange={(checked): void => setIsFillSpans(checked)}
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
<Title light="true">Panel Time Preference</Title>
|
||||
{allowPanelTimePreference && (
|
||||
<Title light="true">Panel Time Preference</Title>
|
||||
)}
|
||||
|
||||
<Space direction="vertical">
|
||||
<TimePreference
|
||||
{...{
|
||||
selectedTime,
|
||||
setSelectedTime,
|
||||
}}
|
||||
/>
|
||||
{allowPanelTimePreference && (
|
||||
<TimePreference
|
||||
{...{
|
||||
selectedTime,
|
||||
setSelectedTime,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<YAxisUnitSelector
|
||||
defaultValue={yAxisUnit}
|
||||
onSelect={setYAxisUnit}
|
||||
fieldLabel={selectedGraphType === 'Value' ? 'Unit' : 'Y Axis Unit'}
|
||||
/>
|
||||
{allowYAxisUnit && (
|
||||
<YAxisUnitSelector
|
||||
defaultValue={yAxisUnit}
|
||||
onSelect={setYAxisUnit}
|
||||
fieldLabel={selectedGraphType === 'Value' ? 'Unit' : 'Y Axis Unit'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedWidget?.panelTypes !== PANEL_TYPES.TABLE && (
|
||||
{allowCreateAlerts && (
|
||||
<Button icon={<UploadOutlined />} onClick={onCreateAlertsHandler}>
|
||||
Create Alerts from Queries
|
||||
</Button>
|
||||
|
@ -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<IField[] | null>(
|
||||
selectedWidget?.selectedLogFields || null,
|
||||
);
|
||||
|
||||
const [selectedTracesFields, setSelectedTracesFields] = useState(
|
||||
selectedWidget?.selectedTracesFields || null,
|
||||
);
|
||||
|
||||
const [softMax, setSoftMax] = useState<number | null>(
|
||||
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}
|
||||
/>
|
||||
</LeftContainerWrapper>
|
||||
|
||||
|
@ -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<SetStateAction<Widgets['selectedLogFields']>>;
|
||||
selectedTracesFields: Widgets['selectedTracesFields'];
|
||||
setSelectedTracesFields?: Dispatch<
|
||||
SetStateAction<Widgets['selectedTracesFields']>
|
||||
>;
|
||||
}
|
||||
|
@ -9,13 +9,17 @@ export default function NoLogs(): JSX.Element {
|
||||
<div className="no-logs-container-content">
|
||||
<img className="eyes-emoji" src="/Images/eyesEmoji.svg" alt="eyes emoji" />
|
||||
<Typography className="no-logs-text">
|
||||
No logs yet.{' '}
|
||||
No logs yet.
|
||||
<span className="sub-text">
|
||||
When we receive logs, they would show up here
|
||||
</span>
|
||||
</Typography>
|
||||
|
||||
<Typography.Link className="send-logs-link">
|
||||
<Typography.Link
|
||||
className="send-logs-link"
|
||||
href="https://signoz.io/docs/userguide/logs/"
|
||||
target="_blank"
|
||||
>
|
||||
Sending Logs to SigNoz <ArrowUpRight size={16} />
|
||||
</Typography.Link>
|
||||
</div>
|
||||
|
@ -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';
|
||||
|
||||
|
@ -26,4 +26,5 @@ export type QueryBuilderProps = {
|
||||
actions?: ReactNode;
|
||||
filterConfigs?: Partial<FilterConfigs>;
|
||||
queryComponents?: { renderOrderBy?: (props: OrderByFilterProps) => ReactNode };
|
||||
isListViewPanel?: boolean;
|
||||
};
|
||||
|
@ -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 (
|
||||
<Row
|
||||
style={{ width: '100%' }}
|
||||
@ -91,21 +124,23 @@ export const QueryBuilder = memo(function QueryBuilder({
|
||||
justify="start"
|
||||
className="query-builder-container"
|
||||
>
|
||||
<div className="new-query-formula-buttons-container">
|
||||
<Button.Group>
|
||||
<Tooltip title="Add Query">
|
||||
<Button disabled={isDisabledQueryButton} onClick={addNewBuilderQuery}>
|
||||
<DatabaseZap size={12} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{!isListViewPanel && (
|
||||
<div className="new-query-formula-buttons-container">
|
||||
<Button.Group>
|
||||
<Tooltip title="Add Query">
|
||||
<Button disabled={isDisabledQueryButton} onClick={addNewBuilderQuery}>
|
||||
<DatabaseZap size={12} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Add Formula">
|
||||
<Button disabled={isDisabledFormulaButton} onClick={addNewFormula}>
|
||||
<Sigma size={12} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Button.Group>
|
||||
</div>
|
||||
<Tooltip title="Add Formula">
|
||||
<Button disabled={isDisabledFormulaButton} onClick={addNewFormula}>
|
||||
<Sigma size={12} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Button.Group>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Col span={23} className="qb-entities-list">
|
||||
<Row>
|
||||
@ -119,49 +154,66 @@ export const QueryBuilder = memo(function QueryBuilder({
|
||||
className="query-builder-queries-formula-container"
|
||||
ref={containerRef}
|
||||
>
|
||||
{currentQuery.builder.queryData.map((query, index) => (
|
||||
<Col
|
||||
key={query.queryName}
|
||||
span={24}
|
||||
className="query"
|
||||
id={`qb-query-${query.queryName}`}
|
||||
>
|
||||
<Query
|
||||
index={index}
|
||||
isAvailableToDisable={isAvailableToDisableQuery}
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
query={query}
|
||||
filterConfigs={filterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
/>
|
||||
</Col>
|
||||
))}
|
||||
{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 && (
|
||||
<Query
|
||||
index={0}
|
||||
isAvailableToDisable={isAvailableToDisableQuery}
|
||||
queryVariant="dropdown"
|
||||
query={currentQuery.builder.queryData[0]}
|
||||
filterConfigs={
|
||||
currentQuery.builder.queryData[0].dataSource === DataSource.TRACES
|
||||
? listViewTracesFilterConfigs
|
||||
: listViewLogFilterConfigs
|
||||
}
|
||||
queryComponents={queryComponents}
|
||||
isListViewPanel
|
||||
/>
|
||||
)}
|
||||
{!isListViewPanel &&
|
||||
currentQuery.builder.queryData.map((query, index) => (
|
||||
<Col
|
||||
key={formula.queryName}
|
||||
key={query.queryName}
|
||||
span={24}
|
||||
className="formula"
|
||||
id={`qb-formula-${formula.queryName}`}
|
||||
className="query"
|
||||
id={`qb-query-${query.queryName}`}
|
||||
>
|
||||
<Formula
|
||||
filterConfigs={filterConfigs}
|
||||
query={query}
|
||||
isAdditionalFilterEnable={isAllMetricDataSource}
|
||||
formula={formula}
|
||||
<Query
|
||||
index={index}
|
||||
isAvailableToDisable={isAvailableToDisableQuery}
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
query={query}
|
||||
filterConfigs={filterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
{!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 (
|
||||
<Col
|
||||
key={formula.queryName}
|
||||
span={24}
|
||||
className="formula"
|
||||
id={`qb-formula-${formula.queryName}`}
|
||||
>
|
||||
<Formula
|
||||
filterConfigs={filterConfigs}
|
||||
query={query}
|
||||
isAdditionalFilterEnable={isAllMetricDataSource}
|
||||
formula={formula}
|
||||
index={index}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
|
||||
<Col span={24} className="divider">
|
||||
@ -171,29 +223,31 @@ export const QueryBuilder = memo(function QueryBuilder({
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
<Col span={1} className="query-builder-mini-map">
|
||||
{currentQuery.builder.queryData.map((query) => (
|
||||
<Button
|
||||
disabled={isDisabledQueryButton}
|
||||
className="query-btn"
|
||||
key={query.queryName}
|
||||
onClick={(): void => handleScrollIntoView('query', query.queryName)}
|
||||
>
|
||||
{query.queryName}
|
||||
</Button>
|
||||
))}
|
||||
{!isListViewPanel && (
|
||||
<Col span={1} className="query-builder-mini-map">
|
||||
{currentQuery.builder.queryData.map((query) => (
|
||||
<Button
|
||||
disabled={isDisabledQueryButton}
|
||||
className="query-btn"
|
||||
key={query.queryName}
|
||||
onClick={(): void => handleScrollIntoView('query', query.queryName)}
|
||||
>
|
||||
{query.queryName}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
{currentQuery.builder.queryFormulas.map((formula) => (
|
||||
<Button
|
||||
disabled={isDisabledFormulaButton}
|
||||
className="formula-btn"
|
||||
key={formula.queryName}
|
||||
onClick={(): void => handleScrollIntoView('formula', formula.queryName)}
|
||||
>
|
||||
{formula.queryName}
|
||||
</Button>
|
||||
))}
|
||||
</Col>
|
||||
{currentQuery.builder.queryFormulas.map((formula) => (
|
||||
<Button
|
||||
disabled={isDisabledFormulaButton}
|
||||
className="formula-btn"
|
||||
key={formula.queryName}
|
||||
onClick={(): void => handleScrollIntoView('formula', formula.queryName)}
|
||||
>
|
||||
{formula.queryName}
|
||||
</Button>
|
||||
))}
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
});
|
||||
|
@ -3,4 +3,5 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export type QueryLabelProps = {
|
||||
onChange: (value: DataSource) => void;
|
||||
isListViewPanel?: boolean;
|
||||
} & Omit<SelectProps, 'onChange'>;
|
||||
|
@ -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<DataSource, string>[] = isListViewPanel
|
||||
? exploreDataSourceMap.map((source) => ({
|
||||
label: transformToUpperCase(source),
|
||||
value: source,
|
||||
}))
|
||||
: dataSourceMap.map((source) => ({
|
||||
label: transformToUpperCase(source),
|
||||
value: source,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Select
|
||||
|
@ -12,6 +12,7 @@ interface QBEntityOptionsProps {
|
||||
onToggleVisibility: () => void;
|
||||
onCollapseEntity: () => void;
|
||||
showDeleteButton: boolean;
|
||||
isListViewPanel?: boolean;
|
||||
}
|
||||
|
||||
export default function QBEntityOptions({
|
||||
@ -22,6 +23,7 @@ export default function QBEntityOptions({
|
||||
onToggleVisibility,
|
||||
onCollapseEntity,
|
||||
showDeleteButton,
|
||||
isListViewPanel = false,
|
||||
}: QBEntityOptionsProps): JSX.Element {
|
||||
return (
|
||||
<Col span={24}>
|
||||
@ -40,10 +42,10 @@ export default function QBEntityOptions({
|
||||
value="query-builder"
|
||||
className="periscope-btn visibility-toggle"
|
||||
onClick={onToggleVisibility}
|
||||
disabled={isListViewPanel}
|
||||
>
|
||||
{entityData.disabled ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className={cx(
|
||||
'periscope-btn',
|
||||
@ -72,3 +74,7 @@ export default function QBEntityOptions({
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
QBEntityOptions.defaultProps = {
|
||||
isListViewPanel: false,
|
||||
};
|
||||
|
@ -6,4 +6,5 @@ export type QueryProps = {
|
||||
isAvailableToDisable: boolean;
|
||||
query: IBuilderQuery;
|
||||
queryVariant: 'static' | 'dropdown';
|
||||
isListViewPanel?: boolean;
|
||||
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './Query.styles.scss';
|
||||
|
||||
import { Col, Input, Row } from 'antd';
|
||||
@ -46,6 +47,7 @@ export const Query = memo(function Query({
|
||||
query,
|
||||
filterConfigs,
|
||||
queryComponents,
|
||||
isListViewPanel = false,
|
||||
}: QueryProps): JSX.Element {
|
||||
const { panelType, currentQuery } = useQueryBuilder();
|
||||
const { pathname } = useLocation();
|
||||
@ -62,7 +64,7 @@ export const Query = memo(function Query({
|
||||
handleChangeDataSource,
|
||||
handleChangeOperator,
|
||||
handleDeleteQuery,
|
||||
} = useQueryOperations({ index, query, filterConfigs });
|
||||
} = useQueryOperations({ index, query, filterConfigs, isListViewPanel });
|
||||
|
||||
const handleChangeAggregateEvery = useCallback(
|
||||
(value: IBuilderQuery['stepInterval']) => {
|
||||
@ -136,8 +138,14 @@ export const Query = memo(function Query({
|
||||
});
|
||||
}
|
||||
|
||||
return <OrderByFilter query={query} onChange={handleChangeOrderByKeys} />;
|
||||
}, [queryComponents, query, handleChangeOrderByKeys]);
|
||||
return (
|
||||
<OrderByFilter
|
||||
query={query}
|
||||
onChange={handleChangeOrderByKeys}
|
||||
isListViewPanel={isListViewPanel}
|
||||
/>
|
||||
);
|
||||
}, [queryComponents, query, handleChangeOrderByKeys, isListViewPanel]);
|
||||
|
||||
const renderAggregateEveryFilter = useCallback(
|
||||
(): JSX.Element | null =>
|
||||
@ -289,6 +297,7 @@ export const Query = memo(function Query({
|
||||
onDelete={handleDeleteQuery}
|
||||
onCollapseEntity={handleToggleCollapsQuery}
|
||||
showDeleteButton={currentQuery.builder.queryData.length > 1}
|
||||
isListViewPanel={isListViewPanel}
|
||||
/>
|
||||
|
||||
{!isCollapse && (
|
||||
@ -302,6 +311,7 @@ export const Query = memo(function Query({
|
||||
onChange={handleChangeDataSource}
|
||||
value={query.dataSource}
|
||||
style={{ minWidth: '5.625rem' }}
|
||||
isListViewPanel={isListViewPanel}
|
||||
/>
|
||||
) : (
|
||||
<FilterLabel label={transformToUpperCase(query.dataSource)} />
|
||||
@ -346,7 +356,7 @@ export const Query = memo(function Query({
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
{!isMetricsDataSource && (
|
||||
{!isMetricsDataSource && !isListViewPanel && (
|
||||
<Col span={11}>
|
||||
<Row gutter={[11, 5]}>
|
||||
<Col flex="5.93rem">
|
||||
@ -368,27 +378,29 @@ export const Query = memo(function Query({
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
<Col span={11} offset={isMetricsDataSource ? 0 : 2}>
|
||||
<Row gutter={[11, 5]}>
|
||||
<Col flex="5.93rem">
|
||||
<FilterLabel
|
||||
label={panelType === PANEL_TYPES.VALUE ? 'Reduce to' : 'Group by'}
|
||||
/>
|
||||
</Col>
|
||||
<Col flex="1 1 12.5rem">
|
||||
{panelType === PANEL_TYPES.VALUE ? (
|
||||
<ReduceToFilter query={query} onChange={handleChangeReduceTo} />
|
||||
) : (
|
||||
<GroupByFilter
|
||||
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
|
||||
query={query}
|
||||
onChange={handleChangeGroupByKeys}
|
||||
{!isListViewPanel && (
|
||||
<Col span={11} offset={isMetricsDataSource ? 0 : 2}>
|
||||
<Row gutter={[11, 5]}>
|
||||
<Col flex="5.93rem">
|
||||
<FilterLabel
|
||||
label={panelType === PANEL_TYPES.VALUE ? 'Reduce to' : 'Group by'}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
{!isTracePanelType && (
|
||||
</Col>
|
||||
<Col flex="1 1 12.5rem">
|
||||
{panelType === PANEL_TYPES.VALUE ? (
|
||||
<ReduceToFilter query={query} onChange={handleChangeReduceTo} />
|
||||
) : (
|
||||
<GroupByFilter
|
||||
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
|
||||
query={query}
|
||||
onChange={handleChangeGroupByKeys}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
{!isTracePanelType && !isListViewPanel && (
|
||||
<Col span={24}>
|
||||
<AdditionalFiltersToggler
|
||||
listOfAdditionalFilter={listOfAdditionalFilters}
|
||||
@ -399,6 +411,13 @@ export const Query = memo(function Query({
|
||||
</AdditionalFiltersToggler>
|
||||
</Col>
|
||||
)}
|
||||
{isListViewPanel && (
|
||||
<Col span={24}>
|
||||
<Row gutter={[0, 11]} justify="space-between">
|
||||
{renderAdditionalFilters()}
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
{panelType !== PANEL_TYPES.LIST && panelType !== PANEL_TYPES.TRACE && (
|
||||
<Row style={{ width: '100%' }}>
|
||||
<Input
|
||||
|
@ -39,7 +39,7 @@ export default function LeftToolbarActions({
|
||||
)}
|
||||
onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.SEARCH)}
|
||||
>
|
||||
<MousePointerSquare size={14} />
|
||||
<MousePointerSquare size={14} data-testid="search-view" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Query Builder">
|
||||
@ -52,7 +52,7 @@ export default function LeftToolbarActions({
|
||||
)}
|
||||
onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER)}
|
||||
>
|
||||
<Atom size={14} />
|
||||
<Atom size={14} data-testid="query-builder-view" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
@ -66,7 +66,7 @@ export default function LeftToolbarActions({
|
||||
)}
|
||||
onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.CLICKHOUSE)}
|
||||
>
|
||||
<Terminal size={14} />
|
||||
<Terminal size={14} data-testid="clickhouse-view" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
export type OrderByFilterProps = {
|
||||
query: IBuilderQuery;
|
||||
onChange: (values: OrderByPayload[]) => void;
|
||||
isListViewPanel?: boolean;
|
||||
};
|
||||
|
||||
export type OrderByFilterValue = {
|
||||
|
@ -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,
|
||||
},
|
||||
);
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
height: 36px;
|
||||
margin-bottom: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
.nav-item-active-marker {
|
||||
|
@ -16,13 +16,16 @@ export default function NavItem({
|
||||
isCollapsed: boolean;
|
||||
item: SidebarItem;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
onClick: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
}): JSX.Element {
|
||||
const { label, icon } = item;
|
||||
|
||||
return (
|
||||
<Tooltip title={isCollapsed ? label : ''} placement="right">
|
||||
<div className={cx('nav-item', isActive ? 'active' : '')} onClick={onClick}>
|
||||
<div
|
||||
className={cx('nav-item', isActive ? 'active' : '')}
|
||||
onClick={(event): void => onClick(event)}
|
||||
>
|
||||
<div className="nav-item-active-marker" />
|
||||
<div className="nav-item-data">
|
||||
<div className="nav-item-icon">{icon}</div>
|
||||
|
@ -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({
|
||||
<div
|
||||
className="brand-logo"
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
onClick={(): void => {
|
||||
onClick={(event: MouseEvent): void => {
|
||||
// Current home page
|
||||
onClickHandler(ROUTES.APPLICATION);
|
||||
onClickHandler(ROUTES.APPLICATION, event);
|
||||
}}
|
||||
>
|
||||
<img src="/Logos/signoz-brand-logo.svg" alt="SigNoz" />
|
||||
@ -314,7 +351,12 @@ function SideNav({
|
||||
|
||||
{isCloudUserVal && (
|
||||
<div className="get-started-nav-items">
|
||||
<Button className="get-started-btn" onClick={onClickGetStarted}>
|
||||
<Button
|
||||
className="get-started-btn"
|
||||
onClick={(event: MouseEvent): void => {
|
||||
onClickGetStarted(event);
|
||||
}}
|
||||
>
|
||||
<RocketIcon size={16} />
|
||||
|
||||
{!collapsed && <> Get Started </>}
|
||||
@ -329,12 +371,8 @@ function SideNav({
|
||||
key={item.key || index}
|
||||
item={item}
|
||||
isActive={activeMenuKey === item.key}
|
||||
onClick={(): void => {
|
||||
if (item.key === ROUTES.SETTINGS) {
|
||||
history.push(settingsRoute);
|
||||
} else if (item) {
|
||||
onClickHandler(item?.key as string);
|
||||
}
|
||||
onClick={(event): void => {
|
||||
handleMenuItemClick(event, item);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@ -366,8 +404,8 @@ function SideNav({
|
||||
key={item?.key || index}
|
||||
item={item}
|
||||
isActive={activeMenuKey === item?.key}
|
||||
onClick={(): void => {
|
||||
handleUserManagentMenuItemClick(item?.key as string);
|
||||
onClick={(event: MouseEvent): void => {
|
||||
handleUserManagentMenuItemClick(item?.key as string, event);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
@ -379,8 +417,12 @@ function SideNav({
|
||||
key={inviteMemberMenuItem.key}
|
||||
item={inviteMemberMenuItem}
|
||||
isActive={activeMenuKey === inviteMemberMenuItem?.key}
|
||||
onClick={(): void => {
|
||||
history.push(`${inviteMemberMenuItem.key}`);
|
||||
onClick={(event: React.MouseEvent): void => {
|
||||
if (isCtrlMetaKey(event)) {
|
||||
openInNewTab(`${inviteMemberMenuItem.key}`);
|
||||
} else {
|
||||
history.push(`${inviteMemberMenuItem.key}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@ -391,8 +433,11 @@ function SideNav({
|
||||
key={ROUTES.MY_SETTINGS}
|
||||
item={userSettingsMenuItem}
|
||||
isActive={activeMenuKey === userSettingsMenuItem?.key}
|
||||
onClick={(): void => {
|
||||
handleUserManagentMenuItemClick(userSettingsMenuItem?.key as string);
|
||||
onClick={(event: MouseEvent): void => {
|
||||
handleUserManagentMenuItemClick(
|
||||
userSettingsMenuItem?.key as string,
|
||||
event,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user