Merge pull request #5288 from SigNoz/release/v0.48.x

Release/v0.48.x
This commit is contained in:
Prashant Shahi 2024-06-20 20:49:47 +05:30 committed by GitHub
commit 9dbef080c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
204 changed files with 4535 additions and 2659 deletions

View File

@ -146,7 +146,7 @@ services:
condition: on-failure condition: on-failure
query-service: query-service:
image: signoz/query-service:0.47.0 image: signoz/query-service:0.48.0
command: command:
[ [
"-config=/root/config/prometheus.yml", "-config=/root/config/prometheus.yml",
@ -186,7 +186,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:0.47.0 image: signoz/frontend:0.48.0
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:0.88.26 image: signoz/signoz-otel-collector:0.102.0
command: command:
[ [
"--config=/etc/otel-collector-config.yaml", "--config=/etc/otel-collector-config.yaml",
@ -237,7 +237,7 @@ services:
- query-service - query-service
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.88.26 image: signoz/signoz-schema-migrator:0.102.0
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure

View File

@ -77,7 +77,16 @@ processors:
# This is added to ensure the uniqueness of the timeseries # This is added to ensure the uniqueness of the timeseries
# Otherwise, identical timeseries produced by multiple replicas of # Otherwise, identical timeseries produced by multiple replicas of
# collectors result in incorrect APM metrics # collectors result in incorrect APM metrics
- name: 'signoz.collector.id' - name: signoz.collector.id
- name: service.version
- name: browser.platform
- name: browser.mobile
- name: k8s.cluster.name
- name: k8s.node.name
- name: k8s.namespace.name
- name: host.name
- name: host.type
- name: container.name
# memory_limiter: # memory_limiter:
# # 80% of maximum memory up to 2G # # 80% of maximum memory up to 2G
# limit_mib: 1500 # limit_mib: 1500
@ -108,6 +117,15 @@ processors:
# Otherwise, identical timeseries produced by multiple replicas of # Otherwise, identical timeseries produced by multiple replicas of
# collectors result in incorrect APM metrics # collectors result in incorrect APM metrics
- name: signoz.collector.id - name: signoz.collector.id
- name: service.version
- name: browser.platform
- name: browser.mobile
- name: k8s.cluster.name
- name: k8s.node.name
- name: k8s.namespace.name
- name: host.name
- name: host.type
- name: container.name
exporters: exporters:
clickhousetraces: clickhousetraces:

View File

@ -66,7 +66,7 @@ services:
- --storage.path=/data - --storage.path=/data
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.26} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.0}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--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` # 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: otel-collector:
container_name: signoz-otel-collector container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:0.88.26 image: signoz/signoz-otel-collector:0.102.0
command: command:
[ [
"--config=/etc/otel-collector-config.yaml", "--config=/etc/otel-collector-config.yaml",

View File

@ -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` # 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: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.47.0} image: signoz/query-service:${DOCKER_TAG:-0.48.0}
container_name: signoz-query-service container_name: signoz-query-service
command: command:
[ [
@ -204,7 +204,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.47.0} image: signoz/frontend:${DOCKER_TAG:-0.48.0}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@ -216,7 +216,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.26} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.0}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
@ -230,7 +230,7 @@ services:
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.26} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.0}
container_name: signoz-otel-collector container_name: signoz-otel-collector
command: command:
[ [

View File

@ -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` # 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: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.47.0} image: signoz/query-service:${DOCKER_TAG:-0.48.0}
container_name: signoz-query-service container_name: signoz-query-service
command: command:
[ [
@ -203,7 +203,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.47.0} image: signoz/frontend:${DOCKER_TAG:-0.48.0}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@ -215,7 +215,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.26} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.0}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
@ -229,7 +229,7 @@ services:
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.26} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.0}
container_name: signoz-otel-collector container_name: signoz-otel-collector
command: command:
[ [

View File

@ -75,7 +75,16 @@ processors:
# This is added to ensure the uniqueness of the timeseries # This is added to ensure the uniqueness of the timeseries
# Otherwise, identical timeseries produced by multiple replicas of # Otherwise, identical timeseries produced by multiple replicas of
# collectors result in incorrect APM metrics # collectors result in incorrect APM metrics
- name: 'signoz.collector.id' - name: signoz.collector.id
- name: service.version
- name: browser.platform
- name: browser.mobile
- name: k8s.cluster.name
- name: k8s.node.name
- name: k8s.namespace.name
- name: host.name
- name: host.type
- name: container.name
# memory_limiter: # memory_limiter:
# # 80% of maximum memory up to 2G # # 80% of maximum memory up to 2G
# limit_mib: 1500 # limit_mib: 1500
@ -111,6 +120,15 @@ processors:
# Otherwise, identical timeseries produced by multiple replicas of # Otherwise, identical timeseries produced by multiple replicas of
# collectors result in incorrect APM metrics # collectors result in incorrect APM metrics
- name: signoz.collector.id - name: signoz.collector.id
- name: service.version
- name: browser.platform
- name: browser.mobile
- name: k8s.cluster.name
- name: k8s.node.name
- name: k8s.namespace.name
- name: host.name
- name: host.type
- name: container.name
extensions: extensions:
health_check: health_check:

View File

@ -14,7 +14,6 @@ import (
"go.signoz.io/signoz/ee/query-service/constants" "go.signoz.io/signoz/ee/query-service/constants"
"go.signoz.io/signoz/ee/query-service/model" "go.signoz.io/signoz/ee/query-service/model"
"go.signoz.io/signoz/pkg/query-service/auth"
baseauth "go.signoz.io/signoz/pkg/query-service/auth" baseauth "go.signoz.io/signoz/pkg/query-service/auth"
basemodel "go.signoz.io/signoz/pkg/query-service/model" basemodel "go.signoz.io/signoz/pkg/query-service/model"
) )
@ -51,7 +50,7 @@ func (ah *APIHandler) loginUser(w http.ResponseWriter, r *http.Request) {
} }
// if all looks good, call auth // if all looks good, call auth
resp, err := auth.Login(ctx, &req) resp, err := baseauth.Login(ctx, &req)
if ah.HandleError(w, err, http.StatusUnauthorized) { if ah.HandleError(w, err, http.StatusUnauthorized) {
return return
} }
@ -130,7 +129,7 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
} else { } else {
// no-sso, validate password // no-sso, validate password
if err := auth.ValidatePassword(req.Password); err != nil { if err := baseauth.ValidatePassword(req.Password); err != nil {
RespondError(w, model.InternalError(fmt.Errorf("password is not in a valid format")), nil) RespondError(w, model.InternalError(fmt.Errorf("password is not in a valid format")), nil)
return return
} }
@ -241,6 +240,11 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
// prepare google callback handler using parsedState - // prepare google callback handler using parsedState -
// which contains redirect URL (front-end endpoint) // which contains redirect URL (front-end endpoint)
callbackHandler, err := domain.PrepareGoogleOAuthProvider(parsedState) callbackHandler, err := domain.PrepareGoogleOAuthProvider(parsedState)
if err != nil {
zap.L().Error("[receiveGoogleAuth] failed to prepare google oauth provider", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri)
return
}
identity, err := callbackHandler.HandleCallback(r) identity, err := callbackHandler.HandleCallback(r)
if err != nil { if err != nil {

View File

@ -21,15 +21,15 @@ import (
// GetMetricResultEE runs the query and returns list of time series // GetMetricResultEE runs the query and returns list of time series
func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string) ([]*basemodel.Series, string, error) { func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string) ([]*basemodel.Series, string, error) {
defer utils.Elapsed("GetMetricResult")() defer utils.Elapsed("GetMetricResult", nil)()
zap.L().Info("Executing metric result query: ", zap.String("query", query)) zap.L().Info("Executing metric result query: ", zap.String("query", query))
var hash string var hash string
// If getSubTreeSpans function is used in the clickhouse query // If getSubTreeSpans function is used in the clickhouse query
if strings.Index(query, "getSubTreeSpans(") != -1 { if strings.Contains(query, "getSubTreeSpans(") {
var err error var err error
query, hash, err = r.getSubTreeSpansCustomFunction(ctx, query, hash) query, hash, err = r.getSubTreeSpansCustomFunction(ctx, query, hash)
if err == fmt.Errorf("No spans found for the given query") { if err == fmt.Errorf("no spans found for the given query") {
return nil, "", nil return nil, "", nil
} }
if err != nil { if err != nil {
@ -183,7 +183,7 @@ func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, qu
if err != nil { if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err)) zap.L().Error("Error in processing sql query", zap.Error(err))
return query, hash, fmt.Errorf("Error in processing sql query") return query, hash, fmt.Errorf("error in processing sql query")
} }
var searchScanResponses []basemodel.SearchSpanDBResponseItem var searchScanResponses []basemodel.SearchSpanDBResponseItem
@ -193,14 +193,14 @@ func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, qu
modelQuery := fmt.Sprintf("SELECT timestamp, traceID, model FROM %s.%s WHERE traceID=$1", r.TraceDB, r.SpansTable) modelQuery := fmt.Sprintf("SELECT timestamp, traceID, model FROM %s.%s WHERE traceID=$1", r.TraceDB, r.SpansTable)
if len(getSpansSubQueryDBResponses) == 0 { if len(getSpansSubQueryDBResponses) == 0 {
return query, hash, fmt.Errorf("No spans found for the given query") return query, hash, fmt.Errorf("no spans found for the given query")
} }
zap.L().Debug("Executing query to fetch all the spans from the same TraceID: ", zap.String("modelQuery", modelQuery)) zap.L().Debug("Executing query to fetch all the spans from the same TraceID: ", zap.String("modelQuery", modelQuery))
err = r.conn.Select(ctx, &searchScanResponses, modelQuery, getSpansSubQueryDBResponses[0].TraceID) err = r.conn.Select(ctx, &searchScanResponses, modelQuery, getSpansSubQueryDBResponses[0].TraceID)
if err != nil { if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err)) zap.L().Error("Error in processing sql query", zap.Error(err))
return query, hash, fmt.Errorf("Error in processing sql query") return query, hash, fmt.Errorf("error in processing sql query")
} }
// Process model to fetch the spans // Process model to fetch the spans
@ -263,6 +263,7 @@ func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, qu
return query, hash, nil return query, hash, nil
} }
//lint:ignore SA4009 return hash is feeded to the query
func processQuery(query string, hash string) (string, string, string) { func processQuery(query string, hash string) (string, string, string) {
re3 := regexp.MustCompile(`getSubTreeSpans`) re3 := regexp.MustCompile(`getSubTreeSpans`)

View File

@ -61,7 +61,7 @@ func SmartTraceAlgorithm(payload []basemodel.SearchSpanResponseItem, targetSpanI
// If the target span is not found, return span not found error // If the target span is not found, return span not found error
if targetSpan == nil { if targetSpan == nil {
return nil, errors.New("Span not found") return nil, errors.New("span not found")
} }
// Build the final result // Build the final result
@ -118,8 +118,8 @@ func SmartTraceAlgorithm(payload []basemodel.SearchSpanResponseItem, targetSpanI
} }
searchSpansResult := []basemodel.SearchSpansResult{{ searchSpansResult := []basemodel.SearchSpansResult{{
Columns: []string{"__time", "SpanId", "TraceId", "ServiceName", "Name", "Kind", "DurationNano", "TagsKeys", "TagsValues", "References", "Events", "HasError"}, Columns: []string{"__time", "SpanId", "TraceId", "ServiceName", "Name", "Kind", "DurationNano", "TagsKeys", "TagsValues", "References", "Events", "HasError"},
Events: make([][]interface{}, len(resultSpansSet)), Events: make([][]interface{}, len(resultSpansSet)),
IsSubTree: true, IsSubTree: true,
}, },
} }
@ -219,7 +219,7 @@ func breadthFirstSearch(spansPtr *model.SpanForTraceDetails, targetId string) (*
} }
for _, child := range current.Children { for _, child := range current.Children {
if ok, _ := visited[child.SpanID]; !ok { if ok := visited[child.SpanID]; !ok {
queue = append(queue, child) queue = append(queue, child)
} }
} }

View File

@ -28,7 +28,6 @@ import (
"go.signoz.io/signoz/ee/query-service/integrations/gateway" "go.signoz.io/signoz/ee/query-service/integrations/gateway"
"go.signoz.io/signoz/ee/query-service/interfaces" "go.signoz.io/signoz/ee/query-service/interfaces"
baseauth "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" v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
licensepkg "go.signoz.io/signoz/ee/query-service/license" licensepkg "go.signoz.io/signoz/ee/query-service/license"
@ -79,9 +78,7 @@ type ServerOptions struct {
// Server runs HTTP api service // Server runs HTTP api service
type Server struct { type Server struct {
serverOptions *ServerOptions serverOptions *ServerOptions
conn net.Listener
ruleManager *rules.Manager ruleManager *rules.Manager
separatePorts bool
// public http router // public http router
httpConn net.Listener httpConn net.Listener
@ -91,9 +88,6 @@ type Server struct {
privateConn net.Listener privateConn net.Listener
privateHTTP *http.Server privateHTTP *http.Server
// feature flags
featureLookup baseint.FeatureLookup
// Usage manager // Usage manager
usageManager *usage.Manager usageManager *usage.Manager
@ -317,7 +311,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server, error) { func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server, error) {
r := mux.NewRouter() r := baseapp.NewRouter()
r.Use(baseapp.LogCommentEnricher) r.Use(baseapp.LogCommentEnricher)
r.Use(setTimeoutMiddleware) r.Use(setTimeoutMiddleware)
@ -344,7 +338,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, error) { func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, error) {
r := mux.NewRouter() r := baseapp.NewRouter()
// add auth middleware // add auth middleware
getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) { getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) {
@ -385,7 +379,7 @@ func loggingMiddleware(next http.Handler) http.Handler {
path, _ := route.GetPathTemplate() path, _ := route.GetPathTemplate()
startTime := time.Now() startTime := time.Now()
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
zap.L().Info(path+"\ttimeTaken:"+time.Now().Sub(startTime).String(), zap.Duration("timeTaken", time.Now().Sub(startTime)), zap.String("path", path)) zap.L().Info(path, zap.Duration("timeTaken", time.Since(startTime)), zap.String("path", path))
}) })
} }
@ -397,7 +391,7 @@ func loggingMiddlewarePrivate(next http.Handler) http.Handler {
path, _ := route.GetPathTemplate() path, _ := route.GetPathTemplate()
startTime := time.Now() startTime := time.Now()
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
zap.L().Info(path+"\tprivatePort: true \ttimeTaken"+time.Now().Sub(startTime).String(), zap.Duration("timeTaken", time.Now().Sub(startTime)), zap.String("path", path), zap.Bool("tprivatePort", true)) zap.L().Info(path, zap.Duration("timeTaken", time.Since(startTime)), zap.String("path", path), zap.Bool("tprivatePort", true))
}) })
} }
@ -711,7 +705,7 @@ func makeRulesManager(
db *sqlx.DB, db *sqlx.DB,
ch baseint.Reader, ch baseint.Reader,
disableRules bool, disableRules bool,
fm baseInterface.FeatureLookup) (*rules.Manager, error) { fm baseint.FeatureLookup) (*rules.Manager, error) {
// create engine // create engine
pqle, err := pqle.FromConfigPath(promConfigPath) pqle, err := pqle.FromConfigPath(promConfigPath)

View File

@ -2,11 +2,6 @@ package signozio
type status string type status string
const (
statusSuccess status = "success"
statusError status = "error"
)
type ActivationResult struct { type ActivationResult struct {
Status status `json:"status"` Status status `json:"status"`
Data *ActivationResponse `json:"data,omitempty"` Data *ActivationResponse `json:"data,omitempty"`

View File

@ -111,7 +111,7 @@ func (r *Repo) UpdatePlanDetails(ctx context.Context,
planDetails string) error { planDetails string) error {
if key == "" { if key == "" {
return fmt.Errorf("Update Plan Details failed: license key is required") return fmt.Errorf("update plan details failed: license key is required")
} }
query := `UPDATE licenses query := `UPDATE licenses

View File

@ -32,7 +32,7 @@ func InitDB(db *sqlx.DB) error {
_, err = db.Exec(table_schema) _, err = db.Exec(table_schema)
if err != nil { if err != nil {
return fmt.Errorf("Error in creating licenses table: %s", err.Error()) return fmt.Errorf("error in creating licenses table: %s", err.Error())
} }
table_schema = `CREATE TABLE IF NOT EXISTS feature_status ( table_schema = `CREATE TABLE IF NOT EXISTS feature_status (
@ -45,7 +45,7 @@ func InitDB(db *sqlx.DB) error {
_, err = db.Exec(table_schema) _, err = db.Exec(table_schema)
if err != nil { if err != nil {
return fmt.Errorf("Error in creating feature_status table: %s", err.Error()) return fmt.Errorf("error in creating feature_status table: %s", err.Error())
} }
return nil return nil

View File

@ -14,7 +14,6 @@ import (
semconv "go.opentelemetry.io/otel/semconv/v1.4.0" semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.signoz.io/signoz/ee/query-service/app" "go.signoz.io/signoz/ee/query-service/app"
"go.signoz.io/signoz/pkg/query-service/auth" "go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/constants"
baseconst "go.signoz.io/signoz/pkg/query-service/constants" baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/migrate" "go.signoz.io/signoz/pkg/query-service/migrate"
"go.signoz.io/signoz/pkg/query-service/version" "go.signoz.io/signoz/pkg/query-service/version"
@ -52,7 +51,8 @@ func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger {
) )
if enableQueryServiceLogOTLPExport { if enableQueryServiceLogOTLPExport {
ctx, _ := context.WithTimeout(ctx, time.Second*30) ctx, cancel := context.WithTimeout(ctx, time.Second*30)
defer cancel()
conn, err := grpc.DialContext(ctx, baseconst.OTLPTarget, grpc.WithBlock(), grpc.WithTransportCredentials(insecure.NewCredentials())) conn, err := grpc.DialContext(ctx, baseconst.OTLPTarget, grpc.WithBlock(), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil { if err != nil {
log.Fatalf("failed to establish connection: %v", err) log.Fatalf("failed to establish connection: %v", err)
@ -148,7 +148,7 @@ func main() {
zap.L().Info("JWT secret key set successfully.") zap.L().Info("JWT secret key set successfully.")
} }
if err := migrate.Migrate(constants.RELATIONAL_DATASOURCE_PATH); err != nil { if err := migrate.Migrate(baseconst.RELATIONAL_DATASOURCE_PATH); err != nil {
zap.L().Error("Failed to migrate", zap.Error(err)) zap.L().Error("Failed to migrate", zap.Error(err))
} else { } else {
zap.L().Info("Migration successful") zap.L().Info("Migration successful")

View File

@ -104,7 +104,7 @@ func (od *OrgDomain) GetSAMLCert() string {
// requesting OAuth and also used in processing response from google // requesting OAuth and also used in processing response from google
func (od *OrgDomain) PrepareGoogleOAuthProvider(siteUrl *url.URL) (sso.OAuthCallbackProvider, error) { func (od *OrgDomain) PrepareGoogleOAuthProvider(siteUrl *url.URL) (sso.OAuthCallbackProvider, error) {
if od.GoogleAuthConfig == nil { if od.GoogleAuthConfig == nil {
return nil, fmt.Errorf("Google auth is not setup correctly for this domain") return nil, fmt.Errorf("GOOGLE OAUTH is not setup correctly for this domain")
} }
return od.GoogleAuthConfig.GetProvider(od.Name, siteUrl) return od.GoogleAuthConfig.GetProvider(od.Name, siteUrl)

View File

@ -53,7 +53,7 @@ func New(dbType string, modelDao dao.ModelDao, licenseRepo *license.Repo, clickh
tenantID := "" tenantID := ""
if len(hostNameRegexMatches) == 2 { if len(hostNameRegexMatches) == 2 {
tenantID = hostNameRegexMatches[1] tenantID = hostNameRegexMatches[1]
tenantID = strings.TrimRight(tenantID, "-clickhouse") tenantID = strings.TrimSuffix(tenantID, "-clickhouse")
} }
m := &Manager{ m := &Manager{

View File

@ -0,0 +1,10 @@
<svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.99715 27.2944C4.70156 27.2944 4.74156 27.6477 4.74156 28.3143C4.74156 28.981 4.70156 29.3543 5.05493 29.3543C5.40831 29.3543 27.7778 29.3143 28.0134 29.2965C28.2489 29.2765 28.1889 28.4143 28.1889 28.081C28.1889 27.6699 28.2467 27.3166 27.9156 27.2966C27.5822 27.2766 5.11494 27.2944 4.99715 27.2944Z" fill="#ED6D30"/>
<path d="M5.07275 21.8602L5.09498 27.3132L27.7956 27.291L27.8467 21.7135L27.3466 21.1536L5.255 21.1158L5.07275 21.8602Z" fill="#F78A51"/>
<path d="M5.53728 21.4707L5.07278 21.8596L5.07056 22.724C5.07056 22.724 5.22169 22.8306 5.37282 22.7551C5.52395 22.6795 5.73508 22.5329 5.92177 22.5173C6.21959 22.4951 6.19514 22.7795 6.48184 22.7795C6.76855 22.7795 7.02858 22.4929 7.27083 22.4929C7.51308 22.4929 7.62421 22.7995 7.88202 22.784C8.13983 22.7684 8.28429 22.5084 8.60655 22.5173C8.86436 22.524 8.90881 22.784 9.22663 22.784C9.54445 22.784 9.70669 22.4818 9.97784 22.4818C10.249 22.4818 10.3379 22.8018 10.6401 22.8018C10.9424 22.8018 11.0246 22.4818 11.3713 22.4818C11.7181 22.4818 11.6892 22.784 11.9759 22.7529C12.2626 22.7218 12.2915 22.4729 12.6382 22.4573C12.9849 22.4418 13.0204 22.784 13.3227 22.784C13.625 22.784 13.6161 22.5373 13.8739 22.5373C14.1317 22.5373 18.9145 22.5262 19.0968 22.5262C19.279 22.5262 19.559 22.8462 19.8613 22.8462C20.1636 22.8462 20.0791 22.504 20.4103 22.4951C20.6081 22.4907 20.9925 22.824 21.2192 22.824C21.4459 22.824 21.5282 22.4818 21.7838 22.4662C22.0393 22.4507 22.4194 22.844 22.7217 22.8129C23.0239 22.7818 22.8728 22.4796 23.0995 22.4507C23.3262 22.4196 23.7796 22.784 24.0818 22.7973C24.3841 22.8129 24.1885 22.404 24.5041 22.404C24.8197 22.404 25.0642 22.7507 25.3953 22.7662C25.7265 22.7818 25.502 22.4196 25.8332 22.3884C26.1643 22.3573 26.4066 22.8418 26.7244 22.8106C27.0422 22.7795 26.9066 22.4329 27.1778 22.4173C27.4489 22.4018 27.8267 22.644 27.8267 22.644L27.8401 21.7063L14.7807 17.582L5.53728 21.4707Z" fill="#ED6D30"/>
<path d="M13.8049 29.3267C13.8049 29.3267 13.8605 22.7804 13.8516 22.6204C13.8405 22.4271 14.0116 22.3804 14.1494 22.3804C14.2871 22.3804 18.8558 22.3804 18.9935 22.3804C19.1313 22.3804 19.2113 22.4827 19.2224 22.6093C19.2335 22.736 19.2002 29.3156 19.2002 29.3156L13.8049 29.3267Z" fill="#51362F"/>
<path d="M4.15465 18.7244C4.15465 18.7244 3.23898 20.7487 3.24787 20.902C3.25676 21.0553 3.51234 21.9864 3.92128 22.0109C4.48135 22.0442 4.58359 21.5531 4.67693 21.5531C4.77028 21.5531 4.89474 22.0331 5.21478 22.0797C5.58816 22.1331 5.85708 21.5331 6.00154 21.5331C6.14601 21.5331 6.21713 22.0553 6.55495 22.0553C6.89277 22.0553 7.25281 21.4909 7.38616 21.502C7.51951 21.5131 7.64842 22.102 7.92401 22.102C8.20182 22.102 8.47296 21.5998 8.71299 21.5753C8.83745 21.5642 8.95525 22.1375 9.18194 22.1464C9.40864 22.1575 9.79535 21.5531 9.99093 21.5531C10.1865 21.5531 10.3399 22.1775 10.6377 22.1486C10.9355 22.1197 11.3378 21.5642 11.48 21.5642C11.6222 21.5642 11.7778 22.1264 12.0112 22.1375C12.2223 22.1464 12.5713 21.6087 12.7135 21.5998C12.8557 21.5909 13.0269 22.1486 13.2625 22.1486C13.498 22.1486 13.7536 21.5442 13.9492 21.5331C14.1448 21.522 14.227 22.102 14.4626 22.102C14.6982 22.102 15.0471 21.5175 15.2627 21.5087C15.4783 21.4975 15.5961 22.0686 15.8117 22.0686C16.0272 22.0686 16.2673 21.4887 16.4206 21.482C16.6584 21.4731 16.8096 22.0464 17.1385 22.0575C17.4674 22.0686 17.6008 21.5042 17.8564 21.5042C18.1119 21.5042 18.1853 22.0375 18.472 22.0486C18.7587 22.0597 18.9943 21.4953 19.2099 21.5042C19.4254 21.5153 19.5677 22.0264 19.8055 22.0264C20.0433 22.0264 20.2767 21.5042 20.4522 21.5131C20.6256 21.5242 20.8634 22.0464 21.099 22.0464C21.3346 22.0464 21.5302 21.5064 21.6435 21.502C21.8613 21.4953 22.0836 22.0664 22.3102 22.0464C22.5369 22.0264 22.7992 21.4642 22.9948 21.4731C23.1904 21.4842 23.4904 22.1108 23.726 22.0909C23.9616 22.0709 24.1616 21.4753 24.3772 21.4842C24.5928 21.4931 24.7661 22.0331 25.0395 22.0331C25.2906 22.0331 25.4306 21.5175 25.6573 21.5064C25.884 21.4953 26.0952 21.9997 26.3308 21.9753C26.5663 21.9509 26.6619 21.482 26.8686 21.4731C27.0731 21.462 27.3753 22.0042 27.6731 21.9931C27.971 21.982 28.1243 21.562 28.2888 21.5531C28.4532 21.5442 28.5955 22.0109 28.9955 22.0042C29.3556 21.9997 29.8267 21.3264 29.7334 20.8554C29.6401 20.3843 28.3599 18.5066 28.3599 18.5066L4.15465 18.7244Z" fill="#6C4D43"/>
<path d="M6.09496 13.357C6.09496 13.357 4.90148 15.0328 4.1925 16.5641C3.48352 18.0954 3.21016 19.0022 3.16571 19.8956C3.12126 20.7691 3.24794 20.9024 3.24794 20.9024L4.54366 19.4867C4.54366 19.4867 4.55699 20.8247 4.65256 20.838C4.74813 20.8513 5.74603 19.4578 5.8127 19.4445C5.8816 19.4311 5.8816 20.8513 5.97717 20.8513C6.07274 20.8513 7.09731 19.4178 7.16621 19.4178C7.2351 19.4178 7.26177 20.838 7.34401 20.838C7.42624 20.838 8.35524 19.3911 8.42414 19.4045C8.49304 19.4178 8.73751 20.9202 8.81975 20.9202C8.90198 20.9202 9.76209 19.3911 9.85765 19.3911C9.95322 19.3911 10.0621 20.9758 10.171 20.9758C10.2799 20.9758 11.1267 19.4467 11.1956 19.4467C11.2645 19.4467 11.5379 20.9625 11.6468 20.9491C11.7557 20.9358 12.5069 19.4467 12.5758 19.4734C12.6447 19.5 12.8225 20.9358 12.9447 20.9358C13.0669 20.9358 13.7226 19.4334 13.8315 19.4334C13.9404 19.4334 14.216 20.8913 14.2982 20.8913C14.3804 20.8913 15.0627 19.4289 15.145 19.4156C15.2272 19.4023 15.665 21.0269 15.8006 21.0269C15.9362 21.0269 16.3474 19.5245 16.4429 19.5378C16.5385 19.5512 17.1808 20.9713 17.2341 20.9713C17.2875 20.9713 17.7675 19.4823 17.8209 19.4823C17.8742 19.4823 18.5165 20.8335 18.6121 20.8491C18.7076 20.8624 19.0632 19.4978 19.1321 19.5245C19.201 19.5512 19.8567 20.958 19.9389 20.9713C20.0211 20.9847 20.3078 19.4956 20.3901 19.4956C20.4723 19.4956 21.3724 21.1336 21.4413 21.1202C21.5102 21.1069 21.5925 19.4667 21.6725 19.4534C21.7547 19.44 22.8326 21.0647 22.9148 21.0513C22.9971 21.038 22.9548 19.3978 23.0104 19.3978C23.066 19.3978 23.9527 20.9269 24.075 20.9136C24.1972 20.9002 24.3061 19.48 24.3884 19.48C24.4706 19.48 25.4529 21.1469 25.5774 21.1336C25.7019 21.1202 25.6041 19.5756 25.6596 19.5623C25.7152 19.5489 26.8198 20.9558 26.8753 20.9424C26.9309 20.9291 26.9153 19.4267 27.0109 19.4134C27.1065 19.4 28.131 20.8758 28.2266 20.8469C28.3222 20.8202 28.3355 19.3445 28.3911 19.3311C28.4466 19.3178 29.7268 20.8535 29.7268 20.8535C29.7268 20.8535 29.9757 19.5178 29.5357 18.2377C29.0956 16.9575 28.0266 15.1595 27.5087 14.395C26.9931 13.6304 26.6909 13.277 26.6909 13.277L14.0648 11.6591L6.09496 13.357Z" fill="#A37F69"/>
<path d="M10.4736 8.22084C10.4736 8.22084 8.78668 9.88105 7.98214 10.8412C7.17759 11.8013 6.09301 13.3548 6.09301 13.3548C6.09301 13.3548 5.69963 15.1728 5.8152 15.1862C5.93299 15.1995 7.08647 13.4615 7.19093 13.4726C7.29539 13.4859 7.02202 15.2239 7.12648 15.2506C7.23093 15.2773 8.51554 13.4482 8.57999 13.4348C8.64444 13.4215 8.3733 15.2373 8.4622 15.2639C8.5511 15.2906 9.85126 13.4482 9.92905 13.4482C10.0068 13.4482 10.1113 15.1484 10.2135 15.1484C10.3158 15.1484 11.1736 13.4237 11.2514 13.4348C11.3292 13.4482 11.5115 15.2128 11.6404 15.2373C11.7693 15.2639 12.3671 13.4082 12.4716 13.3948C12.576 13.3815 12.8339 15.3417 12.9516 15.3417C13.0694 15.3417 13.6917 13.4215 13.7695 13.4215C13.8473 13.4215 14.0429 15.3417 14.1718 15.3417C14.3007 15.3417 14.8852 13.3837 14.963 13.3837C15.0408 13.3837 15.5986 15.2639 15.6898 15.2395C15.7809 15.2128 16.2743 13.3593 16.3654 13.3704C16.4565 13.3837 16.8833 15.1862 17.041 15.2128C17.1966 15.2395 17.6122 13.4615 17.7411 13.4615C17.87 13.4615 18.2079 15.4329 18.3634 15.4329C18.519 15.4329 18.8702 13.4615 18.948 13.4615C19.0257 13.4615 19.7392 15.4084 19.857 15.4195C19.9747 15.4329 20.1037 13.5637 20.2459 13.5504C20.3881 13.5371 21.1549 15.4195 21.2327 15.4062C21.3105 15.3929 21.3749 13.5637 21.4527 13.5504C21.5305 13.5371 22.3995 15.2639 22.5417 15.2639C22.684 15.2639 22.5929 13.4726 22.724 13.4859C22.8529 13.4993 24.1508 15.3662 24.2686 15.3662C24.3864 15.3662 23.9308 13.4193 24.0353 13.3948C24.1397 13.3682 25.5021 15.4706 25.6443 15.4306C25.7866 15.3906 25.2821 13.5237 25.371 13.4971C25.4621 13.4704 26.8756 15.3262 27.0067 15.2751C27.1356 15.2239 26.7 13.277 26.7 13.277C26.7 13.277 25.3976 11.5768 24.7242 10.7478C24.0486 9.91661 22.9862 8.81425 22.9862 8.81425L17.7478 6.19836L10.4736 8.22084Z" fill="#BD9177"/>
<path d="M10.4734 8.2202C10.4734 8.2202 9.83556 9.42236 9.96447 9.49791C10.0934 9.57346 11.6736 8.05576 11.8269 8.09354C11.9803 8.13131 11.3157 9.70012 11.5336 9.75123C11.7514 9.80234 12.7959 8.0291 12.9248 8.05354C13.0515 8.07798 12.6559 9.77567 12.8604 9.84011C13.0649 9.90455 13.945 7.9891 14.085 8.01576C14.225 8.04021 14.1872 9.929 14.3139 9.94233C14.4406 9.95566 15.0918 8.10465 15.1807 8.10465C15.2696 8.10465 15.5252 10.0579 15.6785 10.069C15.8319 10.0823 16.2897 8.03576 16.3919 8.03576C16.4942 8.03576 17.0053 9.96677 17.172 9.96677C17.3387 9.96677 17.4387 8.01799 17.5276 7.98021C17.6165 7.94244 18.3633 9.85122 18.5767 9.85122C18.7611 9.85122 18.4478 7.95132 18.5633 7.92466C18.6789 7.90021 19.7368 9.889 19.9546 9.87789C20.1724 9.86456 19.7946 8.02243 19.8968 8.02243C19.9991 8.02243 21.1681 9.86456 21.3592 9.86456C21.5504 9.86456 20.9592 7.99132 21.0747 7.96466C21.1903 7.94021 22.9305 9.60679 23.0328 9.58013C23.135 9.55568 22.9817 8.81128 22.9817 8.81128C22.9817 8.81128 18.7833 4.49595 16.4342 4.48484C14.0339 4.47151 10.4734 8.2202 10.4734 8.2202Z" fill="#D2A590"/>
</svg>

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -108,7 +108,7 @@
"user_tooltip_more_help": "More details on how to create alerts", "user_tooltip_more_help": "More details on how to create alerts",
"choose_alert_type": "Choose a type for the alert", "choose_alert_type": "Choose a type for the alert",
"metric_based_alert": "Metric based Alert", "metric_based_alert": "Metric based Alert",
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data", "metric_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
"log_based_alert": "Log-based Alert", "log_based_alert": "Log-based Alert",
"log_based_alert_desc": "Send a notification when a condition occurs in the logs data.", "log_based_alert_desc": "Send a notification when a condition occurs in the logs data.",
"traces_based_alert": "Trace-based Alert", "traces_based_alert": "Trace-based Alert",

View File

@ -108,7 +108,7 @@
"user_tooltip_more_help": "More details on how to create alerts", "user_tooltip_more_help": "More details on how to create alerts",
"choose_alert_type": "Choose a type for the alert", "choose_alert_type": "Choose a type for the alert",
"metric_based_alert": "Metric based Alert", "metric_based_alert": "Metric based Alert",
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data", "metric_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
"log_based_alert": "Log-based Alert", "log_based_alert": "Log-based Alert",
"log_based_alert_desc": "Send a notification when a condition occurs in the logs data.", "log_based_alert_desc": "Send a notification when a condition occurs in the logs data.",
"traces_based_alert": "Trace-based Alert", "traces_based_alert": "Trace-based Alert",

View File

@ -178,23 +178,25 @@ function App(): JSX.Element {
}, [pathname]); }, [pathname]);
useEffect(() => { useEffect(() => {
try { if (user && user?.email && user?.userId && user?.name) {
const isThemeAnalyticsSent = getLocalStorageApi( try {
LOCALSTORAGE.THEME_ANALYTICS, const isThemeAnalyticsSent = getLocalStorageApi(
); LOCALSTORAGE.THEME_ANALYTICS_V1,
if (!isThemeAnalyticsSent) { );
trackEvent('Theme Analytics', { if (!isThemeAnalyticsSent) {
theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT, trackEvent('Theme Analytics', {
user: pick(user, ['email', 'userId', 'name']), theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT,
org, user: pick(user, ['email', 'userId', 'name']),
}); org,
setLocalStorageApi(LOCALSTORAGE.THEME_ANALYTICS, 'true'); });
setLocalStorageApi(LOCALSTORAGE.THEME_ANALYTICS_V1, 'true');
}
} catch {
console.error('Failed to parse local storage theme analytics event');
} }
} catch {
console.error('Failed to parse local storage theme analytics event');
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [user]);
return ( return (
<ConfigProvider theme={themeConfig}> <ConfigProvider theme={themeConfig}>

View File

@ -11,6 +11,13 @@ export const ServiceMetricsPage = Loadable(
), ),
); );
export const ServiceTopLevelOperationsPage = Loadable(
() =>
import(
/* webpackChunkName: "ServiceMetricsPage" */ 'pages/ServiceTopLevelOperations'
),
);
export const ServiceMapPage = Loadable( export const ServiceMapPage = Loadable(
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'), () => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
); );

View File

@ -33,6 +33,7 @@ import {
ServiceMapPage, ServiceMapPage,
ServiceMetricsPage, ServiceMetricsPage,
ServicesTablePage, ServicesTablePage,
ServiceTopLevelOperationsPage,
SettingsPage, SettingsPage,
ShortcutsPage, ShortcutsPage,
SignupPage, SignupPage,
@ -84,6 +85,13 @@ const routes: AppRoutes[] = [
isPrivate: true, isPrivate: true,
key: 'SERVICE_METRICS', key: 'SERVICE_METRICS',
}, },
{
path: ROUTES.SERVICE_TOP_LEVEL_OPERATIONS,
exact: true,
component: ServiceTopLevelOperationsPage,
isPrivate: true,
key: 'SERVICE_TOP_LEVEL_OPERATIONS',
},
{ {
path: ROUTES.SERVICE_MAP, path: ROUTES.SERVICE_MAP,
component: ServiceMapPage, component: ServiceMapPage,

View File

@ -3,6 +3,7 @@ import './DropDown.styles.scss';
import { EllipsisOutlined } from '@ant-design/icons'; import { EllipsisOutlined } from '@ant-design/icons';
import { Button, Dropdown, MenuProps } from 'antd'; import { Button, Dropdown, MenuProps } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useState } from 'react';
function DropDown({ element }: { element: JSX.Element[] }): JSX.Element { function DropDown({ element }: { element: JSX.Element[] }): JSX.Element {
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
@ -14,12 +15,24 @@ function DropDown({ element }: { element: JSX.Element[] }): JSX.Element {
}), }),
); );
const [isDdOpen, setDdOpen] = useState<boolean>(false);
return ( return (
<Dropdown menu={{ items }}> <Dropdown
menu={{
items,
onMouseEnter: (): void => setDdOpen(true),
onMouseLeave: (): void => setDdOpen(false),
}}
open={isDdOpen}
>
<Button <Button
type="link" type="link"
className={!isDarkMode ? 'dropdown-button--dark' : 'dropdown-button'} className={!isDarkMode ? 'dropdown-button--dark' : 'dropdown-button'}
onClick={(e): void => e.preventDefault()} onClick={(e): void => {
e.preventDefault();
setDdOpen(true);
}}
> >
<EllipsisOutlined className="dropdown-icon" /> <EllipsisOutlined className="dropdown-icon" />
</Button> </Button>

View File

@ -1,6 +1,7 @@
/* eslint-disable sonarjs/cognitive-complexity */ /* eslint-disable sonarjs/cognitive-complexity */
import './Uplot.styles.scss'; import './Uplot.styles.scss';
import * as Sentry from '@sentry/react';
import { Typography } from 'antd'; import { Typography } from 'antd';
import { ToggleGraphProps } from 'components/Graph/types'; import { ToggleGraphProps } from 'components/Graph/types';
import { LineChart } from 'lucide-react'; import { LineChart } from 'lucide-react';
@ -13,7 +14,6 @@ import {
useImperativeHandle, useImperativeHandle,
useRef, useRef,
} from 'react'; } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import UPlot from 'uplot'; import UPlot from 'uplot';
import { dataMatch, optionsUpdateState } from './utils'; import { dataMatch, optionsUpdateState } from './utils';
@ -139,7 +139,7 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
} }
return ( return (
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}> <Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="uplot-graph-container" ref={targetRef}> <div className="uplot-graph-container" ref={targetRef}>
{data && data[0] && data[0]?.length === 0 ? ( {data && data[0] && data[0]?.length === 0 ? (
<div className="not-found"> <div className="not-found">
@ -147,7 +147,7 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
</div> </div>
) : null} ) : null}
</div> </div>
</ErrorBoundary> </Sentry.ErrorBoundary>
); );
}, },
); );

View File

@ -2,12 +2,24 @@
color: var(--bg-amber-500); color: var(--bg-amber-500);
border-color: var(--bg-amber-500); border-color: var(--bg-amber-500);
.ant-btn:hover { > .ant-btn:hover {
color: var(--bg-amber-400) !important; color: var(--bg-amber-400) !important;
border-color: var(--bg-amber-300) !important; border-color: var(--bg-amber-300) !important;
} }
} }
.lightMode {
.facing-issue-button {
color: var(--bg-vanilla-500);
border-color: var(--bg-vanilla-300);
> .ant-btn:hover {
color: var(--bg-vanilla-500) !important;
border-color: var(--bg-vanilla-300) !important;
}
}
}
.tooltip-overlay { .tooltip-overlay {
text-wrap: nowrap; text-wrap: nowrap;
.ant-tooltip-inner { .ant-tooltip-inner {

View File

@ -18,5 +18,5 @@ export enum LOCALSTORAGE {
DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES', DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES',
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR', SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES', PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
THEME_ANALYTICS = 'THEME_ANALYTICS', THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
} }

View File

@ -40,4 +40,5 @@ export const getComponentForPanelType = (
export const AVAILABLE_EXPORT_PANEL_TYPES = [ export const AVAILABLE_EXPORT_PANEL_TYPES = [
PANEL_TYPES.TIME_SERIES, PANEL_TYPES.TIME_SERIES,
PANEL_TYPES.TABLE, PANEL_TYPES.TABLE,
PANEL_TYPES.LIST,
]; ];

View File

@ -174,8 +174,8 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
sourceNames: alphabet, sourceNames: alphabet,
}), }),
disabled: false, disabled: false,
having: [],
stepInterval: 60, stepInterval: 60,
having: [],
limit: null, limit: null,
orderBy: [], orderBy: [],
groupBy: [], groupBy: [],

View File

@ -2,6 +2,7 @@ const ROUTES = {
SIGN_UP: '/signup', SIGN_UP: '/signup',
LOGIN: '/login', LOGIN: '/login',
SERVICE_METRICS: '/services/:servicename', SERVICE_METRICS: '/services/:servicename',
SERVICE_TOP_LEVEL_OPERATIONS: '/services/:servicename/top-level-operations',
SERVICE_MAP: '/service-map', SERVICE_MAP: '/service-map',
TRACE: '/trace', TRACE: '/trace',
TRACE_DETAIL: '/trace/:id', TRACE_DETAIL: '/trace/:id',

View File

@ -78,6 +78,82 @@ const themeColors = {
mediumVioletRed: '#C71585', mediumVioletRed: '#C71585',
paleGreen: '#98FB98', paleGreen: '#98FB98',
}, },
lightModeColor: {
robin: '#3F5ECC',
dodgerBlueDark: '#0C6EED',
steelgrey: '#2f4b7c',
steelpurple: '#665191',
steelindigo: '#a05195',
steelpink: '#d45087',
steelcoral: '#f95d6a',
steelorange: '#ff7c43',
steelgold: '#ffa600',
steelrust: '#de425b',
steelgreen: '#41967e',
mediumOrchidDark: '#C326FD',
seaBuckthornDark: '#E66E05',
seaGreen: '#219653',
turquoiseBlueDark: '#0099CC',
silverDark: '#757575',
outrageousOrangeDark: '#F9521A',
roseBudDark: '#EB6437',
deepSkyBlueDark: '#0595BD',
royalBlue: '#3366E6',
avocadoDark: '#8E8E29',
mintGreenDark: '#00C700',
chestnut: '#B34D4D',
limaDark: '#6E9900',
olive: '#809900',
beautyBushDark: '#E25555',
danube: '#6680B3',
oliveDrab: '#66991A',
lavenderRoseDark: '#F024BD',
electricLimeDark: '#84A800',
radicalRed: '#FF1A66',
harleyOrange: '#E6331A',
gladeGreen: '#66994D',
hemlock: '#66664D',
vidaLoca: '#4D8000',
rust: '#B33300',
red: '#FF0000', // Adding more colors, we need to get better colors from design team
blue: '#0000FF',
green: '#00FF00',
purple: '#800080',
magentaDark: '#EB00EB',
pinkDark: '#FF3D5E',
brown: '#A52A2A',
teal: '#008080',
limeDark: '#07A207',
maroon: '#800000',
navy: '#000080',
gray: '#808080',
skyBlueDark: '#0CA7E4',
indigo: '#4B0082',
slateGray: '#708090',
chocolate: '#D2691E',
tomato: '#FF6347',
steelBlue: '#4682B4',
peruDark: '#D16E0A',
darkOliveGreen: '#556B2F',
indianRed: '#CD5C5C',
mediumSlateBlue: '#7B68EE',
rosyBrownDark: '#CB4848',
darkSlateGray: '#2F4F4F',
fuchsia: '#FF0AFF',
salmonDark: '#FF432E',
darkSalmonDark: '#D26541',
paleVioletRedDark: '#E83089',
mediumPurple: '#9370DB',
darkOrchid: '#9932CC',
mediumSeaGreenDark: '#109E50',
lightCoralDark: '#F85959',
darkSeaGreenDark: '#509F50',
sandyBrownDark: '#D97117',
darkKhakiDark: '#99900A',
cornflowerBlueDark: '#3371E6',
mediumVioletRed: '#C71585',
paleGreenDark: '#0D910D',
},
errorColor: '#d32f2f', errorColor: '#d32f2f',
royalGrey: '#888888', royalGrey: '#888888',
matterhornGrey: '#555555', matterhornGrey: '#555555',

View File

@ -1,5 +1,5 @@
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { Typography } from 'antd'; import { Tooltip, Typography } from 'antd';
import getAll from 'api/channels/getAll'; import getAll from 'api/channels/getAll';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import TextToolTip from 'components/TextToolTip'; import TextToolTip from 'components/TextToolTip';
@ -52,11 +52,21 @@ function AlertChannels(): JSX.Element {
url="https://signoz.io/docs/userguide/alerts-management/#setting-notification-channel" url="https://signoz.io/docs/userguide/alerts-management/#setting-notification-channel"
/> />
{addNewChannelPermission && ( <Tooltip
<Button onClick={onToggleHandler} icon={<PlusOutlined />}> title={
!addNewChannelPermission
? 'Ask an admin to create alert channel'
: undefined
}
>
<Button
onClick={onToggleHandler}
icon={<PlusOutlined />}
disabled={!addNewChannelPermission}
>
{t('button_new_channel')} {t('button_new_channel')}
</Button> </Button>
)} </Tooltip>
</RightActionContainer> </RightActionContainer>
</ButtonContainer> </ButtonContainer>

View File

@ -3,6 +3,7 @@
/* eslint-disable jsx-a11y/anchor-is-valid */ /* eslint-disable jsx-a11y/anchor-is-valid */
import './AppLayout.styles.scss'; import './AppLayout.styles.scss';
import * as Sentry from '@sentry/react';
import { Flex } from 'antd'; import { Flex } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get'; import getLocalStorageKey from 'api/browser/localstorage/get';
import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs'; import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs';
@ -27,7 +28,6 @@ import {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Helmet } from 'react-helmet-async'; import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQueries } from 'react-query'; import { useQueries } from 'react-query';
@ -342,7 +342,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
/> />
)} )}
<div className={cx('app-content', collapsed ? 'collapsed' : '')}> <div className={cx('app-content', collapsed ? 'collapsed' : '')}>
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}> <Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<LayoutContent> <LayoutContent>
<ChildrenContainer <ChildrenContainer
style={{ style={{
@ -360,7 +360,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
{children} {children}
</ChildrenContainer> </ChildrenContainer>
</LayoutContent> </LayoutContent>
</ErrorBoundary> </Sentry.ErrorBoundary>
</div> </div>
</Flex> </Flex>
</Layout> </Layout>

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-loop-func */ /* eslint-disable @typescript-eslint/no-loop-func */
import './BillingContainer.styles.scss'; import './BillingContainer.styles.scss';
import { CheckCircleOutlined } from '@ant-design/icons'; import { CheckCircleOutlined, CloudDownloadOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { import {
Alert, Alert,
@ -40,6 +40,7 @@ import { isCloudUser } from 'utils/app';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils'; import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph'; import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
import { prepareCsvData } from './BillingUsageGraph/utils';
interface DataType { interface DataType {
key: string; key: string;
@ -371,6 +372,37 @@ export default function BillingContainer(): JSX.Element {
</Typography> </Typography>
); );
const handleCsvDownload = useCallback((): void => {
try {
const csv = prepareCsvData(apiResponse);
if (!csv.csvData || !csv.fileName) {
throw new Error('Invalid CSV data or file name.');
}
const csvBlob = new Blob([csv.csvData], { type: 'text/csv;charset=utf-8;' });
const csvUrl = URL.createObjectURL(csvBlob);
const downloadLink = document.createElement('a');
downloadLink.href = csvUrl;
downloadLink.download = csv.fileName;
document.body.appendChild(downloadLink); // Required for Firefox
downloadLink.click();
// Clean up
downloadLink.remove();
URL.revokeObjectURL(csvUrl); // Release the memory associated with the object URL
notifications.success({
message: 'Download successful',
});
} catch (error) {
console.error('Error downloading the CSV file:', error);
notifications.error({
message: SOMETHING_WENT_WRONG,
});
}
}, [apiResponse, notifications]);
return ( return (
<div className="billing-container"> <div className="billing-container">
<Flex vertical style={{ marginBottom: 16 }}> <Flex vertical style={{ marginBottom: 16 }}>
@ -399,17 +431,29 @@ export default function BillingContainer(): JSX.Element {
</Typography.Text> </Typography.Text>
) : null} ) : null}
</Flex> </Flex>
<Button <Flex gap={20}>
type="primary" <Button
size="middle" type="dashed"
loading={isLoadingBilling || isLoadingManageBilling} size="middle"
disabled={isLoading} loading={isLoadingBilling || isLoadingManageBilling}
onClick={handleBilling} disabled={isLoading || isFetchingBillingData}
> onClick={handleCsvDownload}
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription icon={<CloudDownloadOutlined />}
? t('upgrade_plan') >
: t('manage_billing')} Download CSV
</Button> </Button>
<Button
type="primary"
size="middle"
loading={isLoadingBilling || isLoadingManageBilling}
disabled={isLoading}
onClick={handleBilling}
>
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
? t('upgrade_plan')
: t('manage_billing')}
</Button>
</Flex>
</Flex> </Flex>
{licensesData?.payload?.onTrial && {licensesData?.payload?.onTrial &&

View File

@ -166,6 +166,7 @@ export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
), ),
yAxisUnit: '', yAxisUnit: '',
isBillingUsageGraphs: true, isBillingUsageGraphs: true,
isDarkMode,
}), }),
], ],
}), }),

View File

@ -0,0 +1,129 @@
import dayjs from 'dayjs';
export interface QuantityData {
metric: string;
values: [number, number][];
queryName: string;
legend: string;
quantity: number[];
unit: string;
}
interface DataPoint {
date: string;
metric: {
total: number;
cost: number;
};
trace: {
total: number;
cost: number;
};
log: {
total: number;
cost: number;
};
}
interface CsvData {
Date: string;
'Metrics Vol (Mn samples)': number;
'Metrics Cost ($)': number;
'Traces Vol (GBs)': number;
'Traces Cost ($)': number;
'Logs Vol (GBs)': number;
'Logs Cost ($)': number;
}
const formatDate = (timestamp: number): string =>
dayjs.unix(timestamp).format('MM/DD/YYYY');
const getQuantityData = (
data: QuantityData[],
metricName: string,
): QuantityData => {
const defaultData: QuantityData = {
metric: metricName,
values: [],
queryName: metricName,
legend: metricName,
quantity: [],
unit: '',
};
return data.find((d) => d.metric === metricName) || defaultData;
};
const generateCsvData = (quantityData: QuantityData[]): any[] => {
const convertData = (data: QuantityData[]): DataPoint[] => {
const metricsData = getQuantityData(data, 'Metrics');
const tracesData = getQuantityData(data, 'Traces');
const logsData = getQuantityData(data, 'Logs');
const timestamps = metricsData.values.map((value) => value[0]);
return timestamps.map((timestamp, index) => {
const date = formatDate(timestamp);
return {
date,
metric: {
total: metricsData.quantity[index] ?? 0,
cost: metricsData.values[index]?.[1] ?? 0,
},
trace: {
total: tracesData.quantity[index] ?? 0,
cost: tracesData.values[index]?.[1] ?? 0,
},
log: {
total: logsData.quantity[index] ?? 0,
cost: logsData.values[index]?.[1] ?? 0,
},
};
});
};
const formattedData = convertData(quantityData);
// Calculate totals
const totals = formattedData.reduce(
(acc, dataPoint) => {
acc.metric.total += dataPoint.metric.total;
acc.metric.cost += dataPoint.metric.cost;
acc.trace.total += dataPoint.trace.total;
acc.trace.cost += dataPoint.trace.cost;
acc.log.total += dataPoint.log.total;
acc.log.cost += dataPoint.log.cost;
return acc;
},
{
metric: { total: 0, cost: 0 },
trace: { total: 0, cost: 0 },
log: { total: 0, cost: 0 },
},
);
const csvData: CsvData[] = formattedData.map((dataPoint) => ({
Date: dataPoint.date,
'Metrics Vol (Mn samples)': parseFloat(dataPoint.metric.total.toFixed(2)),
'Metrics Cost ($)': parseFloat(dataPoint.metric.cost.toFixed(2)),
'Traces Vol (GBs)': parseFloat(dataPoint.trace.total.toFixed(2)),
'Traces Cost ($)': parseFloat(dataPoint.trace.cost.toFixed(2)),
'Logs Vol (GBs)': parseFloat(dataPoint.log.total.toFixed(2)),
'Logs Cost ($)': parseFloat(dataPoint.log.cost.toFixed(2)),
}));
// Add totals row
csvData.push({
Date: 'Total',
'Metrics Vol (Mn samples)': parseFloat(totals.metric.total.toFixed(2)),
'Metrics Cost ($)': parseFloat(totals.metric.cost.toFixed(2)),
'Traces Vol (GBs)': parseFloat(totals.trace.total.toFixed(2)),
'Traces Cost ($)': parseFloat(totals.trace.cost.toFixed(2)),
'Logs Vol (GBs)': parseFloat(totals.log.total.toFixed(2)),
'Logs Cost ($)': parseFloat(totals.log.cost.toFixed(2)),
});
return csvData;
};
export default generateCsvData;

View File

@ -1,6 +1,12 @@
import { UsageResponsePayloadProps } from 'api/billing/getUsage';
import dayjs from 'dayjs';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { isEmpty, isNull } from 'lodash-es'; import { isEmpty, isNull } from 'lodash-es';
import { unparse } from 'papaparse';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import generateCsvData, { QuantityData } from './generateCsvData';
export const convertDataToMetricRangePayload = ( export const convertDataToMetricRangePayload = (
data: any, data: any,
): MetricRangePayloadProps => { ): MetricRangePayloadProps => {
@ -58,10 +64,7 @@ export const convertDataToMetricRangePayload = (
}; };
}; };
export function fillMissingValuesForQuantities( export function quantityDataArr(data: any, timestampArray: number[]): any[] {
data: any,
timestampArray: number[],
): MetricRangePayloadProps {
const { result } = data.data; const { result } = data.data;
const transformedResultArr: any[] = []; const transformedResultArr: any[] = [];
@ -76,6 +79,14 @@ export function fillMissingValuesForQuantities(
); );
transformedResultArr.push({ ...item, quantity: quantityArray }); transformedResultArr.push({ ...item, quantity: quantityArray });
}); });
return transformedResultArr;
}
export function fillMissingValuesForQuantities(
data: any,
timestampArray: number[],
): MetricRangePayloadProps {
const transformedResultArr = quantityDataArr(data, timestampArray);
return { return {
data: { data: {
@ -85,3 +96,36 @@ export function fillMissingValuesForQuantities(
}, },
}; };
} }
const formatDate = (timestamp: number): string =>
dayjs.unix(timestamp).format('MM/DD/YYYY');
export function csvFileName(csvData: QuantityData[]): string {
if (!csvData.length) {
return `billing-usage.csv`;
}
const { values } = csvData[0];
const timestamps = values.map((item) => item[0]);
const startDate = formatDate(Math.min(...timestamps));
const endDate = formatDate(Math.max(...timestamps));
return `billing_usage_(${startDate}-${endDate}).csv`;
}
export function prepareCsvData(
data: Partial<UsageResponsePayloadProps>,
): {
csvData: string;
fileName: string;
} {
const graphCompatibleData = convertDataToMetricRangePayload(data);
const chartData = getUPlotChartData(graphCompatibleData);
const quantityMapArr = quantityDataArr(graphCompatibleData, chartData[0]);
return {
csvData: unparse(generateCsvData(quantityMapArr)),
fileName: csvFileName(quantityMapArr),
};
}

View File

@ -12,6 +12,30 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
const optionList = getOptionList(t); const optionList = getOptionList(t);
function handleRedirection(option: AlertTypes): void {
let url = '';
switch (option) {
case AlertTypes.METRICS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples';
break;
case AlertTypes.LOGS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples';
break;
case AlertTypes.TRACES_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples';
break;
case AlertTypes.EXCEPTIONS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples';
break;
default:
break;
}
window.open(url, '_blank');
}
const renderOptions = useMemo( const renderOptions = useMemo(
() => ( () => (
<> <>
@ -23,7 +47,16 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
onSelect(option.selection); onSelect(option.selection);
}} }}
> >
{option.description} {option.description}{' '}
<Typography.Link
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
handleRedirection(option.selection);
}}
>
Click here to see how to create a sample alert.
</Typography.Link>{' '}
</AlertTypeCard> </AlertTypeCard>
))} ))}
</> </>

View File

@ -130,7 +130,7 @@ export const exceptionAlertDefaults: AlertDef = {
disabled: false, disabled: false,
}, },
}, },
queryType: EQueryType.QUERY_BUILDER, queryType: EQueryType.CLICKHOUSE,
panelType: PANEL_TYPES.TIME_SERIES, panelType: PANEL_TYPES.TIME_SERIES,
unit: undefined, unit: undefined,
}, },

View File

@ -1,18 +1,27 @@
.explorer-options-container {
position: fixed;
bottom: 24px;
left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear;
display: flex;
gap: 16px;
background-color: transparent;
}
.hide-update { .hide-update {
left: calc(50% - 72px) !important; left: calc(50% - 72px) !important;
} }
.explorer-update { .explorer-update {
position: fixed; display: inline-flex;
bottom: 24px;
left: calc(50% - 352px);
display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
padding: 10px 12px; padding: 10px 12px;
border-radius: 50px; border-radius: 50px;
border: 1px solid var(--bg-slate-400); border: 1px solid var(--bg-slate-400);
background: rgba(22, 24, 29, 0.6); background: rgba(22, 24, 29, 0.6);
box-shadow: 4px 4px 16px 4px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
.action-icon { .action-icon {
@ -38,16 +47,10 @@
} }
.explorer-options { .explorer-options {
position: fixed;
bottom: 24px;
left: calc(50% + 240px);
padding: 10px 12px; padding: 10px 12px;
transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear;
border: 1px solid var(--bg-slate-400); border: 1px solid var(--bg-slate-400);
border-radius: 50px; border-radius: 50px;
background: rgba(22, 24, 29, 0.6); background: rgba(22, 24, 29, 0.6);
box-shadow: 4px 4px 16px 4px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
cursor: default; cursor: default;
@ -124,7 +127,7 @@
.app-content { .app-content {
&.collapsed { &.collapsed {
.explorer-options { .explorer-options-container {
left: calc(50% + 72px); left: calc(50% + 72px);
} }
} }

View File

@ -289,7 +289,7 @@ function ExplorerOptions({
const isEditDeleteSupported = allowedRoles.includes(role as string); const isEditDeleteSupported = allowedRoles.includes(role as string);
return ( return (
<> <div className="explorer-options-container">
{isQueryUpdated && !isExplorerOptionHidden && ( {isQueryUpdated && !isExplorerOptionHidden && (
<div <div
className={cx( className={cx(
@ -493,7 +493,7 @@ function ExplorerOptions({
onExport={onExport} onExport={onExport}
/> />
</Modal> </Modal>
</> </div>
); );
} }

View File

@ -1,7 +1,17 @@
import { Form, Select, Switch } from 'antd'; import './FormAlertRules.styles.scss';
import { useEffect, useState } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Form, Select, Switch, Tooltip } from 'antd';
import getChannels from 'api/channels/getAll';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import useFetch from 'hooks/useFetch';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { AlertDef, Labels } from 'types/api/alerts/def'; import { AlertDef, Labels } from 'types/api/alerts/def';
import AppReducer from 'types/reducer/app';
import { requireErrorMessage } from 'utils/form/requireErrorMessage'; import { requireErrorMessage } from 'utils/form/requireErrorMessage';
import { popupContainer } from 'utils/selectPopupContainer'; import { popupContainer } from 'utils/selectPopupContainer';
@ -31,6 +41,13 @@ function BasicInfo({
}: BasicInfoProps): JSX.Element { }: BasicInfoProps): JSX.Element {
const { t } = useTranslation('alerts'); const { t } = useTranslation('alerts');
const channels = useFetch(getChannels);
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [addNewChannelPermission] = useComponentPermission(
['add_new_channel'],
role,
);
const [ const [
shouldBroadCastToAllChannels, shouldBroadCastToAllChannels,
setShouldBroadCastToAllChannels, setShouldBroadCastToAllChannels,
@ -54,6 +71,11 @@ function BasicInfo({
}); });
}; };
const noChannels = channels.payload?.length === 0;
const handleCreateNewChannels = useCallback(() => {
window.open(ROUTES.CHANNELS_NEW, '_blank');
}, []);
return ( return (
<> <>
<StepHeading> {t('alert_form_step3')} </StepHeading> <StepHeading> {t('alert_form_step3')} </StepHeading>
@ -137,32 +159,74 @@ function BasicInfo({
name="alert_all_configured_channels" name="alert_all_configured_channels"
label="Alert all the configured channels" label="Alert all the configured channels"
> >
<Switch <Tooltip
checked={shouldBroadCastToAllChannels} title={
onChange={handleBroadcastToAllChannels} noChannels
/> ? 'No channels. Ask an admin to create a notification channel'
: undefined
}
placement="right"
>
<Switch
checked={shouldBroadCastToAllChannels}
onChange={handleBroadcastToAllChannels}
disabled={noChannels || !!channels.loading}
/>
</Tooltip>
</FormItemMedium> </FormItemMedium>
{!shouldBroadCastToAllChannels && ( {!shouldBroadCastToAllChannels && (
<FormItemMedium <Tooltip
label="Notification Channels" title={
name="notification_channels" noChannels
required ? 'No channels. Ask an admin to create a notification channel'
rules={[ : undefined
{ required: true, message: requireErrorMessage(t('field_alert_name')) }, }
]} placement="right"
> >
<ChannelSelect <FormItemMedium
disabled={shouldBroadCastToAllChannels} label="Notification Channels"
currentValue={alertDef.preferredChannels} name="notification_channels"
onSelectChannels={(preferredChannels): void => { required
setAlertDef({ rules={[
...alertDef, { required: true, message: requireErrorMessage(t('field_alert_name')) },
preferredChannels, ]}
}); >
}} <ChannelSelect
/> disabled={
</FormItemMedium> shouldBroadCastToAllChannels || noChannels || !!channels.loading
}
currentValue={alertDef.preferredChannels}
channels={channels}
onSelectChannels={(preferredChannels): void => {
setAlertDef({
...alertDef,
preferredChannels,
});
}}
/>
</FormItemMedium>
</Tooltip>
)}
{noChannels && (
<Tooltip
title={
!addNewChannelPermission
? 'Ask an admin to create a notification channel'
: undefined
}
placement="right"
>
<Button
onClick={handleCreateNewChannels}
icon={<PlusOutlined />}
className="create-notification-btn"
disabled={!addNewChannelPermission}
>
Create a notification channel
</Button>
</Tooltip>
)} )}
</FormContainer> </FormContainer>
</> </>

View File

@ -1,9 +1,9 @@
import { Select } from 'antd'; import { Select } from 'antd';
import getChannels from 'api/channels/getAll'; import { State } from 'hooks/useFetch';
import useFetch from 'hooks/useFetch';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PayloadProps } from 'types/api/channels/getAll';
import { StyledSelect } from './styles'; import { StyledSelect } from './styles';
@ -11,38 +11,42 @@ export interface ChannelSelectProps {
disabled?: boolean; disabled?: boolean;
currentValue?: string[]; currentValue?: string[];
onSelectChannels: (s: string[]) => void; onSelectChannels: (s: string[]) => void;
channels: State<PayloadProps | undefined>;
} }
function ChannelSelect({ function ChannelSelect({
disabled, disabled,
currentValue, currentValue,
onSelectChannels, onSelectChannels,
channels,
}: ChannelSelectProps): JSX.Element | null { }: ChannelSelectProps): JSX.Element | null {
// init namespace for translations // init namespace for translations
const { t } = useTranslation('alerts'); const { t } = useTranslation('alerts');
const { loading, payload, error, errorMessage } = useFetch(getChannels);
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const handleChange = (value: string[]): void => { const handleChange = (value: string[]): void => {
onSelectChannels(value); onSelectChannels(value);
}; };
if (error && errorMessage !== '') { if (channels.error && channels.errorMessage !== '') {
notifications.error({ notifications.error({
message: 'Error', message: 'Error',
description: errorMessage, description: channels.errorMessage,
}); });
} }
const renderOptions = (): ReactNode[] => { const renderOptions = (): ReactNode[] => {
const children: ReactNode[] = []; const children: ReactNode[] = [];
if (loading || payload === undefined || payload.length === 0) { if (
channels.loading ||
channels.payload === undefined ||
channels.payload.length === 0
) {
return children; return children;
} }
payload.forEach((o) => { channels.payload.forEach((o) => {
children.push( children.push(
<Select.Option key={o.id} value={o.name}> <Select.Option key={o.id} value={o.name}>
{o.name} {o.name}
@ -55,7 +59,7 @@ function ChannelSelect({
return ( return (
<StyledSelect <StyledSelect
disabled={disabled} disabled={disabled}
status={error ? 'error' : ''} status={channels.error ? 'error' : ''}
mode="multiple" mode="multiple"
style={{ width: '100%' }} style={{ width: '100%' }}
placeholder={t('placeholder_channel_select')} placeholder={t('placeholder_channel_select')}

View File

@ -21,6 +21,22 @@
} }
} }
.info-help-btns {
display: grid;
grid-template-columns: auto auto;
gap: 12px;
margin-top: 20px;
.doc-redirection-btn {
color: var(--bg-aqua-500) !important;
border-color: var(--bg-aqua-500) !important;
}
.facing-issue-btn {
width: 100% !important;
}
}
.lightMode { .lightMode {
.main-container { .main-container {
.plot-tag { .plot-tag {
@ -47,9 +63,15 @@
} }
} }
} }
.info-help-btns {
.doc-redirection-btn {
color: var(--bg-aqua-600) !important;
border-color: var(--bg-aqua-600) !important;
}
}
} }
.facing-issue-btn { .create-notification-btn {
margin-top: 20px; box-shadow: none;
width: 100%;
} }

View File

@ -2,6 +2,7 @@ import './FormAlertRules.styles.scss';
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons'; import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
import { import {
Button,
Col, Col,
FormInstance, FormInstance,
Modal, Modal,
@ -22,13 +23,13 @@ import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters'; import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag'; import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history'; import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi'; import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi'; import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEqual } from 'lodash-es';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query'; import { useQueryClient } from 'react-query';
@ -69,14 +70,15 @@ function FormAlertRules({
// init namespace for translations // init namespace for translations
const { t } = useTranslation('alerts'); const { t } = useTranslation('alerts');
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector< const { selectedTime: globalSelectedInterval } = useSelector<
AppState, AppState,
GlobalReducer GlobalReducer
>((state) => state.globalTime); >((state) => state.globalTime);
const urlQuery = useUrlQuery(); const urlQuery = useUrlQuery();
const panelType = urlQuery.get(QueryParams.panelTypes) as PANEL_TYPES | null; // In case of alert the panel types should always be "Graph" only
const panelType = PANEL_TYPES.TIME_SERIES;
const { const {
currentQuery, currentQuery,
@ -102,6 +104,13 @@ function FormAlertRules({
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue); const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
const [yAxisUnit, setYAxisUnit] = useState<string>(currentQuery.unit || ''); const [yAxisUnit, setYAxisUnit] = useState<string>(currentQuery.unit || '');
useEffect(() => {
if (!isEqual(currentQuery.unit, yAxisUnit)) {
setYAxisUnit(currentQuery.unit || '');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentQuery.unit]);
// initQuery contains initial query when component was mounted // initQuery contains initial query when component was mounted
const initQuery = useMemo(() => initialValue.condition.compositeQuery, [ const initQuery = useMemo(() => initialValue.condition.compositeQuery, [
initialValue, initialValue,
@ -183,7 +192,9 @@ function FormAlertRules({
} }
const query: Query = { ...currentQuery, queryType: val }; const query: Query = { ...currentQuery, queryType: val };
redirectWithQueryBuilderData(updateStepInterval(query, maxTime, minTime)); // update step interval is removed from here as if the user enters
// any value we will use that rather than auto update
redirectWithQueryBuilderData(query);
}; };
const { notifications } = useNotifications(); const { notifications } = useNotifications();
@ -245,7 +256,7 @@ function FormAlertRules({
if ( if (
!currentQuery.builder.queryData || !currentQuery.builder.queryData ||
currentQuery.builder.queryData.length === 0 currentQuery.builder.queryData?.length === 0
) { ) {
notifications.error({ notifications.error({
message: 'Error', message: 'Error',
@ -502,6 +513,31 @@ function FormAlertRules({
const isRuleCreated = !ruleId || ruleId === 0; const isRuleCreated = !ruleId || ruleId === 0;
function handleRedirection(option: AlertTypes): void {
let url = '';
switch (option) {
case AlertTypes.METRICS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
break;
case AlertTypes.LOGS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
break;
case AlertTypes.TRACES_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
break;
case AlertTypes.EXCEPTIONS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
break;
default:
break;
}
window.open(url, '_blank');
}
return ( return (
<> <>
{Element} {Element}
@ -532,7 +568,7 @@ function FormAlertRules({
queryCategory={currentQuery.queryType} queryCategory={currentQuery.queryType}
setQueryCategory={onQueryCategoryChange} setQueryCategory={onQueryCategoryChange}
alertType={alertType || AlertTypes.METRICS_BASED_ALERT} alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
runQuery={handleRunQuery} runQuery={(): void => handleRunQuery(true)}
alertDef={alertDef} alertDef={alertDef}
panelType={panelType || PANEL_TYPES.TIME_SERIES} panelType={panelType || PANEL_TYPES.TIME_SERIES}
key={currentQuery.queryType} key={currentQuery.queryType}
@ -585,22 +621,33 @@ function FormAlertRules({
</StyledLeftContainer> </StyledLeftContainer>
<Col flex="1 1 300px"> <Col flex="1 1 300px">
<UserGuide queryType={currentQuery.queryType} /> <UserGuide queryType={currentQuery.queryType} />
<FacingIssueBtn <div className="info-help-btns">
attributes={{ <Button
alert: alertDef?.alert, style={{ height: 32 }}
alertType: alertDef?.alertType, onClick={(): void =>
id: ruleId, handleRedirection(alertDef?.alertType as AlertTypes)
ruleType: alertDef?.ruleType, }
state: (alertDef as any)?.state, className="doc-redirection-btn"
panelType, >
screen: isRuleCreated ? 'Edit Alert' : 'New Alert', Check an example alert
}} </Button>
className="facing-issue-btn" <FacingIssueBtn
eventName="Alert: Facing Issues in alert" attributes={{
buttonText="Need help with this alert?" alert: alertDef?.alert,
message={alertHelpMessage(alertDef, ruleId)} alertType: alertDef?.alertType,
onHoverText="Click here to get help with this alert" id: ruleId,
/> ruleType: alertDef?.ruleType,
state: (alertDef as any)?.state,
panelType,
screen: isRuleCreated ? 'Edit Alert' : 'New Alert',
}}
className="facing-issue-btn"
eventName="Alert: Facing Issues in alert"
buttonText="Need help with this alert?"
message={alertHelpMessage(alertDef, ruleId)}
onHoverText="Click here to get help with this alert"
/>
</div>
</Col> </Col>
</PanelContainer> </PanelContainer>
</> </>

View File

@ -15,7 +15,6 @@ import {
} from 'container/NewWidget/RightContainer/timeItems'; } from 'container/NewWidget/RightContainer/timeItems';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper'; import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { useChartMutable } from 'hooks/useChartMutable'; import { useChartMutable } from 'hooks/useChartMutable';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
@ -71,7 +70,7 @@ function FullView({
enum: widget?.timePreferance || 'GLOBAL_TIME', enum: widget?.timePreferance || 'GLOBAL_TIME',
}); });
const updatedQuery = useStepInterval(widget?.query); const updatedQuery = widget?.query;
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => { const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
if (widget.panelTypes !== PANEL_TYPES.LIST) { if (widget.panelTypes !== PANEL_TYPES.LIST) {
@ -204,7 +203,7 @@ function FullView({
<div <div
className={cx('graph-container', { className={cx('graph-container', {
disabled: isDashboardLocked, disabled: isDashboardLocked,
'height-widget': widget?.mergeAllActiveQueries, 'height-widget': widget?.mergeAllActiveQueries || widget?.stackedBarChart,
'list-graph-container': isListView, 'list-graph-container': isListView,
})} })}
ref={fullViewRef} ref={fullViewRef}

View File

@ -131,15 +131,22 @@ function WidgetGraphComponent({
const uuid = v4(); const uuid = v4();
// this is added to make sure the cloned panel is of the same dimensions as the original one
const originalPanelLayout = selectedDashboard.data.layout?.find(
(l) => l.i === widget.id,
);
// added the cloned panel on the top as it is given most priority when arranging
// in the layout. React_grid_layout assigns priority from top, hence no random position for cloned panel
const layout = [ const layout = [
...(selectedDashboard.data.layout || []),
{ {
i: uuid, i: uuid,
w: 6, w: originalPanelLayout?.w || 6,
x: 0, x: 0,
h: 6, h: originalPanelLayout?.h || 6,
y: 0, y: 0,
}, },
...(selectedDashboard.data.layout || []),
]; ];
updateDashboardMutation.mutateAsync( updateDashboardMutation.mutateAsync(

View File

@ -3,7 +3,6 @@ import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config'; import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { useIntersectionObserver } from 'hooks/useIntersectionObserver'; import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
@ -90,7 +89,7 @@ function GridCardGraph({
} }
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]); }, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
const updatedQuery = useStepInterval(widget?.query); const updatedQuery = widget?.query;
const isEmptyWidget = const isEmptyWidget =
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget); widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
@ -109,6 +108,7 @@ function GridCardGraph({
query: updatedQuery, query: updatedQuery,
globalSelectedInterval, globalSelectedInterval,
variables: getDashboardVariables(variables), variables: getDashboardVariables(variables),
fillGaps: widget.fillSpans,
}; };
} }
updatedQuery.builder.queryData[0].pageSize = 10; updatedQuery.builder.queryData[0].pageSize = 10;
@ -123,6 +123,7 @@ function GridCardGraph({
limit: updatedQuery.builder.queryData[0].limit || 0, limit: updatedQuery.builder.queryData[0].limit || 0,
}, },
}, },
fillGaps: widget.fillSpans,
}; };
}); });
@ -153,6 +154,7 @@ function GridCardGraph({
widget?.query, widget?.query,
widget?.panelTypes, widget?.panelTypes,
widget.timePreferance, widget.timePreferance,
widget.fillSpans,
requestData, requestData,
], ],
retry(failureCount, error): boolean { retry(failureCount, error): boolean {

View File

@ -62,10 +62,11 @@ function GridTableComponent({
mutateDataSource = mutateDataSource.map( mutateDataSource = mutateDataSource.map(
(val): RowData => { (val): RowData => {
const newValue = val; const newValue = { ...val };
Object.keys(val).forEach((k) => { Object.keys(val).forEach((k) => {
if (columnUnits[k]) { if (columnUnits[k]) {
newValue[k] = getYAxisFormattedValue(String(val[k]), columnUnits[k]); newValue[k] = getYAxisFormattedValue(String(val[k]), columnUnits[k]);
newValue[`${k}_without_unit`] = val[k];
} }
}); });
return newValue; return newValue;
@ -81,7 +82,6 @@ function GridTableComponent({
applyColumnUnits, applyColumnUnits,
originalDataSource, originalDataSource,
]); ]);
useEffect(() => { useEffect(() => {
if (tableProcessedDataRef) { if (tableProcessedDataRef) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign

View File

@ -0,0 +1,37 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import { ArrowRightOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
interface AlertInfoCardProps {
header: string;
subheader: string;
link: string;
}
function AlertInfoCard({
header,
subheader,
link,
}: AlertInfoCardProps): JSX.Element {
return (
<div
className="alert-info-card"
onClick={(): void => {
window.open(link, '_blank');
}}
>
<div className="alert-card-text">
<Typography.Text className="alert-card-text-header">
{header}
</Typography.Text>
<Typography.Text className="alert-card-text-subheader">
{subheader}
</Typography.Text>
</div>
<ArrowRightOutlined />
</div>
);
}
export default AlertInfoCard;

View File

@ -0,0 +1,251 @@
.alert-list-container {
margin-top: 104px;
margin-bottom: 30px;
display: flex;
justify-content: center;
width: 100%;
.alert-list-view-content {
width: calc(100% - 30px);
max-width: 836px;
.alert-list-title-container {
.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;
}
}
.empty-alert-info-container {
display: flex;
padding: 71px 193.5px;
justify-content: center;
align-items: center;
border-radius: 6px;
border: 1px dashed var(--bg-slate-500);
margin-top: 16px;
.alert-content {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
.heading {
display: flex;
flex-direction: column;
gap: 4px;
.icons {
color: white;
}
.empty-alert-action {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 24px; /* 171.429% */
letter-spacing: -0.07px;
}
.empty-info {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 24px;
letter-spacing: -0.07px;
}
}
.action-container {
display: flex;
gap: 24px;
align-items: center;
padding-top: 24px;
padding-bottom: 24px;
width: 100%;
}
}
}
.get-started-text {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
margin-top: 24px;
margin-bottom: 24px;
width: 100%;
.ant-divider::before,
.ant-divider::after {
border-bottom: 2px dotted var(--bg-slate-300);
border-top: 2px dotted var(--bg-slate-300);
height: 8px;
}
.ant-typography {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 166.667% */
letter-spacing: 0.48px;
text-transform: uppercase;
padding-top: 8px;
}
}
.alert-info-card {
display: flex;
padding: 16px;
justify-content: space-between;
align-items: center;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
margin-bottom: 16px;
&:hover {
cursor: pointer;
}
.alert-card-text {
display: flex;
gap: 2px;
flex-direction: column;
.alert-card-text-header {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.alert-card-text-subheader {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
}
}
}
}
}
.info-text {
color: var(--bg-robin-400) !important;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 16px; /* 133.333% */
letter-spacing: -0.06px;
}
.info-link-container {
.anticon {
color: var(--bg-robin-400);
}
:hover {
cursor: pointer;
}
}
.lightMode {
.alert-list-container {
.alert-list-view-content {
.alert-list-title-container {
.title {
color: var(--bg-slate-400);
}
.subtitle {
color: var(--bg-slate-100);
}
}
.empty-alert-info-container {
border: 1px dashed var(--bg-vanilla-400);
.alert-content {
.heading {
.icons {
color: white;
}
.empty-alert-action {
color: var(--bg-slate-100);
}
.empty-info {
color: var(--bg-slate-400);
}
}
}
}
.get-started-text {
.ant-divider::before,
.ant-divider::after {
border-bottom: 2px dotted var(--bg-vanilla-400);
border-top: 2px dotted var(--bg-vanilla-400);
}
.ant-typography {
color: var(--bg-slate-100);
}
}
.alert-info-card {
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);
.alert-card-text {
.alert-card-text-header {
color: var(--bg-slate-400);
}
.alert-card-text-subheader {
color: var(--bg-slate-100);
}
}
}
}
}
.info-text {
color: var(--bg-robin-600) !important;
}
.info-link-container {
.anticon {
color: var(--bg-robin-400);
}
}
}

View File

@ -0,0 +1,127 @@
import './AlertsEmptyState.styles.scss';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Divider, Typography } from 'antd';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import AlertInfoCard from './AlertInfoCard';
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
import InfoLinkText from './InfoLinkText';
export function AlertsEmptyState(): JSX.Element {
const { t } = useTranslation('common');
const { role, featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const [addNewAlert] = useComponentPermission(
['add_new_alert', 'action'],
role,
);
const { notifications: notificationsApi } = useNotifications();
const handleError = useCallback((): void => {
notificationsApi.error({
message: t('something_went_wrong'),
});
}, [notificationsApi, t]);
const [loading, setLoading] = useState(false);
const onClickNewAlertHandler = useCallback(() => {
setLoading(true);
featureResponse
.refetch()
.then(() => {
setLoading(false);
history.push(ROUTES.ALERTS_NEW);
})
.catch(handleError)
.finally(() => setLoading(false));
}, [featureResponse, handleError]);
return (
<div className="alert-list-container">
<div className="alert-list-view-content">
<div className="alert-list-title-container">
<Typography.Title className="title">Alert Rules</Typography.Title>
<Typography.Text className="subtitle">
Create and manage alert rules for your resources.
</Typography.Text>
</div>
<section className="empty-alert-info-container">
<div className="alert-content">
<section className="heading">
<img
src="/Icons/alert_emoji.svg"
alt="alert-header"
style={{ height: '32px', width: '32px' }}
/>
<div>
<Typography.Text className="empty-info">
No Alert rules yet.{' '}
</Typography.Text>
<Typography.Text className="empty-alert-action">
Create an Alert Rule to get started
</Typography.Text>
</div>
</section>
<div className="action-container">
<Button
className="add-alert-btn"
onClick={onClickNewAlertHandler}
icon={<PlusOutlined />}
disabled={!addNewAlert}
loading={loading}
type="primary"
data-testid="add-alert"
>
New Alert Rule
</Button>
<InfoLinkText
infoText="Watch a tutorial on creating a sample alert"
link="https://youtu.be/xjxNIqiv4_M"
leftIconVisible
rightIconVisible
/>
</div>
{ALERT_INFO_LINKS.map((info) => (
<InfoLinkText
key={info.link}
infoText={info.infoText}
link={info.link}
leftIconVisible={info.leftIconVisible}
rightIconVisible={info.rightIconVisible}
/>
))}
</div>
</section>
<div className="get-started-text">
<Divider>
<Typography.Text className="get-started-text">
Or get started with these sample alerts
</Typography.Text>
</Divider>
</div>
{ALERT_CARDS.map((card) => (
<AlertInfoCard
key={card.link}
header={card.header}
subheader={card.subheader}
link={card.link}
/>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,31 @@
import { ArrowRightOutlined, PlayCircleFilled } from '@ant-design/icons';
import { Flex, Typography } from 'antd';
interface InfoLinkTextProps {
infoText: string;
link: string;
leftIconVisible: boolean;
rightIconVisible: boolean;
}
function InfoLinkText({
infoText,
link,
leftIconVisible,
rightIconVisible,
}: InfoLinkTextProps): JSX.Element {
return (
<Flex
onClick={(): void => {
window.open(link, '_blank');
}}
className="info-link-container"
>
{leftIconVisible && <PlayCircleFilled />}
<Typography.Text className="info-text">{infoText}</Typography.Text>
{rightIconVisible && <ArrowRightOutlined rotate={315} />}
</Flex>
);
}
export default InfoLinkText;

View File

@ -0,0 +1,50 @@
export const ALERT_INFO_LINKS = [
{
infoText: 'How to create Metrics-based alerts',
link:
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-empty-page',
leftIconVisible: false,
rightIconVisible: true,
},
{
infoText: 'How to create Log-based alerts',
link:
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-empty-page',
leftIconVisible: false,
rightIconVisible: true,
},
{
infoText: 'How to create Trace-based alerts',
link:
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-empty-page',
leftIconVisible: false,
rightIconVisible: true,
},
];
export const ALERT_CARDS = [
{
header: 'Alert on high memory usage',
subheader: "Monitor your host's memory usage",
link:
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-empty-page#1-alert-when-memory-usage-for-host-goes-above-400-mb-or-any-fixed-memory',
},
{
header: 'Alert on slow external API calls',
subheader: 'Monitor your external API calls',
link:
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-empty-page#1-alert-when-external-api-latency-p90-is-over-1-second-for-last-5-mins',
},
{
header: 'Alert on high percentage of timeout errors in logs',
subheader: 'Monitor your logs for errors',
link:
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-empty-page#1-alert-when-percentage-of-redis-timeout-error-logs-greater-than-7-in-last-5-mins',
},
{
header: 'Alert on high error percentage of an endpoint',
subheader: 'Monitor your API endpoint',
link:
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-empty-page#3-alert-when-the-error-percentage-for-an-endpoint-exceeds-5',
},
];

View File

@ -55,6 +55,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
role, role,
); );
const [editLoader, setEditLoader] = useState<boolean>(false);
const [cloneLoader, setCloneLoader] = useState<boolean>(false);
const params = useUrlQuery(); const params = useUrlQuery();
const orderColumnParam = params.get('columnKey'); const orderColumnParam = params.get('columnKey');
const orderQueryParam = params.get('order'); const orderQueryParam = params.get('order');
@ -113,6 +116,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
}, [featureResponse, handleError]); }, [featureResponse, handleError]);
const onEditHandler = (record: GettableAlert) => (): void => { const onEditHandler = (record: GettableAlert) => (): void => {
setEditLoader(true);
featureResponse featureResponse
.refetch() .refetch()
.then(() => { .then(() => {
@ -129,9 +133,11 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
params.set(QueryParams.ruleId, record.id.toString()); params.set(QueryParams.ruleId, record.id.toString());
setEditLoader(false);
history.push(`${ROUTES.EDIT_ALERTS}?${params.toString()}`); history.push(`${ROUTES.EDIT_ALERTS}?${params.toString()}`);
}) })
.catch(handleError); .catch(handleError)
.finally(() => setEditLoader(false));
}; };
const onCloneHandler = ( const onCloneHandler = (
@ -143,33 +149,41 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
}; };
const apiReq = { data: copyAlert }; const apiReq = { data: copyAlert };
const response = await saveAlertApi(apiReq); try {
setCloneLoader(true);
const response = await saveAlertApi(apiReq);
if (response.statusCode === 200) { if (response.statusCode === 200) {
notificationsApi.success({ notificationsApi.success({
message: 'Success', message: 'Success',
description: 'Alert cloned successfully', description: 'Alert cloned successfully',
}); });
const { data: refetchData, status } = await refetch(); const { data: refetchData, status } = await refetch();
if (status === 'success' && refetchData.payload) { if (status === 'success' && refetchData.payload) {
setData(refetchData.payload || []); setData(refetchData.payload || []);
setTimeout(() => { setTimeout(() => {
const clonedAlert = refetchData.payload[refetchData.payload.length - 1]; const clonedAlert = refetchData.payload[refetchData.payload.length - 1];
params.set(QueryParams.ruleId, String(clonedAlert.id)); params.set(QueryParams.ruleId, String(clonedAlert.id));
history.push(`${ROUTES.EDIT_ALERTS}?${params.toString()}`); history.push(`${ROUTES.EDIT_ALERTS}?${params.toString()}`);
}, 2000); }, 2000);
} }
if (status === 'error') { if (status === 'error') {
notificationsApi.error({
message: t('something_went_wrong'),
});
}
} else {
notificationsApi.error({ notificationsApi.error({
message: t('something_went_wrong'), message: 'Error',
description: response.error || t('something_went_wrong'),
}); });
} }
} else { } catch (error) {
notificationsApi.error({ handleError();
message: 'Error', console.error(error);
description: response.error || t('something_went_wrong'), } finally {
}); setCloneLoader(false);
} }
}; };
@ -314,10 +328,20 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
setData={setData} setData={setData}
id={id} id={id}
/>, />,
<ColumnButton key="2" onClick={onEditHandler(record)} type="link"> <ColumnButton
key="2"
onClick={onEditHandler(record)}
type="link"
loading={editLoader}
>
Edit Edit
</ColumnButton>, </ColumnButton>,
<ColumnButton key="3" onClick={onCloneHandler(record)} type="link"> <ColumnButton
key="3"
onClick={onCloneHandler(record)}
type="link"
loading={cloneLoader}
>
Clone Clone
</ColumnButton>, </ColumnButton>,
<DeleteAlert <DeleteAlert

View File

@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { AlertsEmptyState } from './AlertsEmptyState/AlertsEmptyState';
import ListAlert from './ListAlert'; import ListAlert from './ListAlert';
function ListAlertRules(): JSX.Element { function ListAlertRules(): JSX.Element {
@ -45,6 +46,10 @@ function ListAlertRules(): JSX.Element {
); );
} }
if (status === 'success' && !data.payload?.length) {
return <AlertsEmptyState />;
}
// in case of loading // in case of loading
if (isLoading || !data?.payload) { if (isLoading || !data?.payload) {
return <Spinner height="75vh" tip="Loading Rules..." />; return <Spinner height="75vh" tip="Loading Rules..." />;

View File

@ -27,5 +27,8 @@ export const ColumnButton = styled(ButtonComponent)`
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
margin-right: 1.5em; margin-right: 1.5em;
width: 100%;
display: flex;
align-items: center;
} }
`; `;

View File

@ -53,6 +53,7 @@ import {
Search, Search,
} from 'lucide-react'; } from 'lucide-react';
import { handleContactSupport } from 'pages/Integrations/utils'; import { handleContactSupport } from 'pages/Integrations/utils';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { import {
ChangeEvent, ChangeEvent,
Key, Key,
@ -91,6 +92,11 @@ function DashboardsList(): JSX.Element {
const { role } = useSelector<AppState, AppReducer>((state) => state.app); const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const {
listSortOrder: sortOrder,
setListSortOrder: setSortOrder,
} = useDashboard();
const [action, createNewDashboard] = useComponentPermission( const [action, createNewDashboard] = useComponentPermission(
['action', 'create_new_dashboards'], ['action', 'create_new_dashboards'],
role, role,
@ -116,18 +122,9 @@ function DashboardsList(): JSX.Element {
); );
const params = useUrlQuery(); const params = useUrlQuery();
const orderColumnParam = params.get('columnKey');
const orderQueryParam = params.get('order');
const paginationParam = params.get('page');
const searchParams = params.get('search'); const searchParams = params.get('search');
const [searchString, setSearchString] = useState<string>(searchParams || ''); const [searchString, setSearchString] = useState<string>(searchParams || '');
const [sortOrder, setSortOrder] = useState({
columnKey: orderColumnParam,
order: orderQueryParam,
pagination: paginationParam,
});
const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => { const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => {
const dashboardDynamicColumnsString = localStorage.getItem('dashboard'); const dashboardDynamicColumnsString = localStorage.getItem('dashboard');
let dashboardDynamicColumns: DashboardDynamicColumns = { let dashboardDynamicColumns: DashboardDynamicColumns = {
@ -198,7 +195,6 @@ function DashboardsList(): JSX.Element {
}, [sortOrder]); }, [sortOrder]);
const sortHandle = (key: string): void => { const sortHandle = (key: string): void => {
console.log(dashboards);
if (!dashboards) return; if (!dashboards) return;
if (key === 'createdAt') { if (key === 'createdAt') {
sortDashboardsByCreatedAt(dashboards); sortDashboardsByCreatedAt(dashboards);
@ -225,13 +221,29 @@ function DashboardsList(): JSX.Element {
} }
useEffect(() => { useEffect(() => {
sortDashboardsByCreatedAt(dashboardListResponse);
const filteredDashboards = filterDashboard( const filteredDashboards = filterDashboard(
searchString, searchString,
dashboardListResponse, dashboardListResponse,
); );
setDashboards(filteredDashboards || []); if (sortOrder.columnKey === 'updatedAt') {
}, [dashboardListResponse, searchString]); sortDashboardsByUpdatedAt(filteredDashboards || []);
} else if (sortOrder.columnKey === 'createdAt') {
sortDashboardsByCreatedAt(filteredDashboards || []);
} else if (sortOrder.columnKey === 'null') {
setSortOrder({
columnKey: 'updatedAt',
order: 'descend',
pagination: sortOrder.pagination || '1',
});
sortDashboardsByUpdatedAt(filteredDashboards || []);
}
}, [
dashboardListResponse,
searchString,
setSortOrder,
sortOrder.columnKey,
sortOrder.pagination,
]);
const [newDashboardState, setNewDashboardState] = useState({ const [newDashboardState, setNewDashboardState] = useState({
loading: false, loading: false,
@ -687,7 +699,16 @@ function DashboardsList(): JSX.Element {
New Dashboard New Dashboard
</Button> </Button>
</Dropdown> </Dropdown>
<Button type="text" className="learn-more"> <Button
type="text"
className="learn-more"
onClick={(): void => {
window.open(
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
'_blank',
);
}}
>
Learn more Learn more
</Button> </Button>
<ArrowUpRight size={16} className="learn-more-arrow" /> <ArrowUpRight size={16} className="learn-more-arrow" />
@ -807,6 +828,7 @@ function DashboardsList(): JSX.Element {
showTotal: showPaginationItem, showTotal: showPaginationItem,
showSizeChanger: false, showSizeChanger: false,
onChange: (page): void => handlePageSizeUpdate(page), onChange: (page): void => handlePageSizeUpdate(page),
current: Number(sortOrder.pagination),
defaultCurrent: Number(sortOrder.pagination) || 1, defaultCurrent: Number(sortOrder.pagination) || 1,
} }
} }

View File

@ -30,8 +30,8 @@ export const constructCompositeQuery = ({
}: GetDefaultCompositeQueryParams): Query => ({ }: GetDefaultCompositeQueryParams): Query => ({
...query, ...query,
builder: { builder: {
...query.builder, ...query?.builder,
queryData: query.builder.queryData.map((item) => ({ queryData: query?.builder?.queryData?.map((item) => ({
...initialQueryData, ...initialQueryData,
...item, ...item,
...customQueryData, ...customQueryData,

View File

@ -42,7 +42,7 @@ export const prepareQueryByFilter = (
...query, ...query,
builder: { builder: {
...query.builder, ...query.builder,
queryData: query.builder.queryData.map((item) => ({ queryData: query.builder.queryData?.map((item) => ({
...item, ...item,
filters: value ? getFilter(item.filters, tagFilter, value) : item.filters, filters: value ? getFilter(item.filters, tagFilter, value) : item.filters,
})), })),
@ -57,7 +57,7 @@ export const getQueryWithoutFilterId = (query: Query): Query => {
...query, ...query,
builder: { builder: {
...query.builder, ...query.builder,
queryData: query.builder.queryData.map((item) => ({ queryData: query.builder.queryData?.map((item) => ({
...item, ...item,
filters: { filters: {
...item.filters, ...item.filters,

View File

@ -10,7 +10,7 @@ import { JSONViewProps } from './LogDetailedView.types';
import { aggregateAttributesResourcesToString } from './utils'; import { aggregateAttributesResourcesToString } from './utils';
function JSONView({ logData }: JSONViewProps): JSX.Element { function JSONView({ logData }: JSONViewProps): JSX.Element {
const [isWrapWord, setIsWrapWord] = useState<boolean>(false); const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
const LogJsonData = useMemo( const LogJsonData = useMemo(
() => aggregateAttributesResourcesToString(logData), () => aggregateAttributesResourcesToString(logData),
@ -22,7 +22,7 @@ function JSONView({ logData }: JSONViewProps): JSX.Element {
const options: EditorProps['options'] = { const options: EditorProps['options'] = {
automaticLayout: true, automaticLayout: true,
readOnly: true, readOnly: true,
wordWrap: 'on', wordWrap: isWrapWord ? 'on' : 'off',
minimap: { minimap: {
enabled: false, enabled: false,
}, },
@ -68,7 +68,7 @@ function JSONView({ logData }: JSONViewProps): JSX.Element {
return ( return (
<div className="json-view-container"> <div className="json-view-container">
<MEditor <MEditor
value={isWrapWord ? JSON.stringify(LogJsonData) : LogJsonData} value={LogJsonData}
language="json" language="json"
options={options} options={options}
onChange={(): void => {}} onChange={(): void => {}}

View File

@ -35,7 +35,7 @@ function Overview({
onClickActionItem, onClickActionItem,
isListViewPanel = false, isListViewPanel = false,
}: Props): JSX.Element { }: Props): JSX.Element {
const [isWrapWord, setIsWrapWord] = useState<boolean>(false); const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false); const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
const [isAttributesExpanded, setIsAttributesExpanded] = useState<boolean>( const [isAttributesExpanded, setIsAttributesExpanded] = useState<boolean>(
true, true,
@ -48,7 +48,7 @@ function Overview({
automaticLayout: true, automaticLayout: true,
readOnly: true, readOnly: true,
height: '40vh', height: '40vh',
wordWrap: 'on', wordWrap: isWrapWord ? 'on' : 'off',
minimap: { minimap: {
enabled: false, enabled: false,
}, },
@ -118,8 +118,8 @@ function Overview({
children: ( children: (
<div className="logs-body-content"> <div className="logs-body-content">
<MEditor <MEditor
value={isWrapWord ? JSON.stringify(logData.body) : logData.body} value={logData.body}
language={isWrapWord ? 'placetext' : 'json'} language="json"
options={options} options={options}
onChange={(): void => {}} onChange={(): void => {}}
height="20vh" height="20vh"
@ -143,7 +143,7 @@ function Overview({
</div> </div>
</div> </div>
), ),
extra: <Tag className="tag">{isWrapWord ? 'Raw' : 'JSON'}</Tag>, // extra: <Tag className="tag">JSON</Tag>,
className: 'collapse-content', className: 'collapse-content',
}, },
]} ]}

View File

@ -47,7 +47,7 @@ function LogExplorerQuerySection({
const isTable = panelTypes === PANEL_TYPES.TABLE; const isTable = panelTypes === PANEL_TYPES.TABLE;
const isList = panelTypes === PANEL_TYPES.LIST; const isList = panelTypes === PANEL_TYPES.LIST;
const config: QueryBuilderProps['filterConfigs'] = { const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: isTable, isDisabled: true }, stepInterval: { isHidden: isTable, isDisabled: false },
having: { isHidden: isList, isDisabled: true }, having: { isHidden: isList, isDisabled: true },
filters: { filters: {
customKey: 'body', customKey: 'body',

View File

@ -40,7 +40,7 @@ export const getRequestData = ({
...query, ...query,
builder: { builder: {
...query.builder, ...query.builder,
queryData: query.builder.queryData.map((item) => ({ queryData: query.builder.queryData?.map((item) => ({
...item, ...item,
...paginateData, ...paginateData,
pageSize, pageSize,

View File

@ -37,7 +37,7 @@ import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData'; import useUrlQueryData from 'hooks/useUrlQueryData';
import { FlatLogData } from 'lib/logs/flatLogData'; import { FlatLogData } from 'lib/logs/flatLogData';
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData'; import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
import { defaultTo, isEmpty, omit } from 'lodash-es'; import { cloneDeep, defaultTo, isEmpty, omit, set } from 'lodash-es';
import { Sliders } from 'lucide-react'; import { Sliders } from 'lucide-react';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils'; import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
@ -117,6 +117,12 @@ function LogsExplorerViews({
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null; return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
}, [stagedQuery]); }, [stagedQuery]);
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: initialDataSource || DataSource.LOGS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});
const orderByTimestamp: OrderByPayload | null = useMemo(() => { const orderByTimestamp: OrderByPayload | null = useMemo(() => {
const timestampOrderBy = listQuery?.orderBy.find( const timestampOrderBy = listQuery?.orderBy.find(
(item) => item.columnName === 'timestamp', (item) => item.columnName === 'timestamp',
@ -174,10 +180,10 @@ function LogsExplorerViews({
() => () =>
updateAllQueriesOperators( updateAllQueriesOperators(
currentQuery || initialQueriesMap.logs, currentQuery || initialQueriesMap.logs,
PANEL_TYPES.TIME_SERIES, selectedPanelType,
DataSource.LOGS, DataSource.LOGS,
), ),
[currentQuery, updateAllQueriesOperators], [currentQuery, selectedPanelType, updateAllQueriesOperators],
); );
const handleModeChange = (panelType: PANEL_TYPES): void => { const handleModeChange = (panelType: PANEL_TYPES): void => {
@ -309,6 +315,14 @@ function LogsExplorerViews({
isLoading: isUpdateDashboardLoading, isLoading: isUpdateDashboardLoading,
} = useUpdateDashboard(); } = useUpdateDashboard();
const getUpdatedQueryForExport = useCallback((): Query => {
const updatedQuery = cloneDeep(currentQuery);
set(updatedQuery, 'builder.queryData[0].pageSize', 10);
return updatedQuery;
}, [currentQuery]);
const handleExport = useCallback( const handleExport = useCallback(
(dashboard: Dashboard | null): void => { (dashboard: Dashboard | null): void => {
if (!dashboard || !panelType) return; if (!dashboard || !panelType) return;
@ -319,11 +333,17 @@ function LogsExplorerViews({
const widgetId = v4(); const widgetId = v4();
const query =
panelType === PANEL_TYPES.LIST
? getUpdatedQueryForExport()
: exportDefaultQuery;
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery( const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
dashboard, dashboard,
exportDefaultQuery, query,
widgetId, widgetId,
panelTypeParam, panelTypeParam,
options.selectColumns,
); );
updateDashboard(updatedDashboard, { updateDashboard(updatedDashboard, {
@ -353,7 +373,7 @@ function LogsExplorerViews({
} }
const dashboardEditView = generateExportToDashboardLink({ const dashboardEditView = generateExportToDashboardLink({
query: exportDefaultQuery, query,
panelType: panelTypeParam, panelType: panelTypeParam,
dashboardId: data.payload?.uuid || '', dashboardId: data.payload?.uuid || '',
widgetId, widgetId,
@ -365,7 +385,9 @@ function LogsExplorerViews({
}); });
}, },
[ [
getUpdatedQueryForExport,
exportDefaultQuery, exportDefaultQuery,
options.selectColumns,
history, history,
notifications, notifications,
panelType, panelType,
@ -460,12 +482,6 @@ function LogsExplorerViews({
selectedView, selectedView,
]); ]);
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: initialDataSource || DataSource.METRICS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (!stagedQuery) return []; if (!stagedQuery) return [];

View File

@ -5,7 +5,7 @@
height: 100%; height: 100%;
.resize-table { .resize-table {
height: calc(100% - 40px); height: calc(100% - 70px);
overflow: scroll; overflow: scroll;
overflow-x: hidden; overflow-x: hidden;

View File

@ -27,6 +27,7 @@ import {
handleNonInQueryRange, handleNonInQueryRange,
onGraphClickHandler, onGraphClickHandler,
onViewTracePopupClick, onViewTracePopupClick,
useGetAPMToTracesQueries,
} from './util'; } from './util';
function DBCall(): JSX.Element { function DBCall(): JSX.Element {
@ -96,6 +97,11 @@ function DBCall(): JSX.Element {
[servicename, tagFilterItems], [servicename, tagFilterItems],
); );
const apmToTraceQuery = useGetAPMToTracesQueries({
servicename,
isDBCall: true,
});
return ( return (
<Row gutter={24}> <Row gutter={24}>
<Col span={12}> <Col span={12}>
@ -107,6 +113,7 @@ function DBCall(): JSX.Element {
servicename, servicename,
selectedTraceTags, selectedTraceTags,
timestamp: selectedTimeStamp, timestamp: selectedTimeStamp,
apmToTraceQuery,
})} })}
> >
View Traces View Traces
@ -139,6 +146,7 @@ function DBCall(): JSX.Element {
servicename, servicename,
selectedTraceTags, selectedTraceTags,
timestamp: selectedTimeStamp, timestamp: selectedTimeStamp,
apmToTraceQuery,
})} })}
> >
View Traces View Traces

View File

@ -27,6 +27,7 @@ import {
handleNonInQueryRange, handleNonInQueryRange,
onGraphClickHandler, onGraphClickHandler,
onViewTracePopupClick, onViewTracePopupClick,
useGetAPMToTracesQueries,
} from './util'; } from './util';
function External(): JSX.Element { function External(): JSX.Element {
@ -138,6 +139,11 @@ function External(): JSX.Element {
[servicename, tagFilterItems], [servicename, tagFilterItems],
); );
const apmToTraceQuery = useGetAPMToTracesQueries({
servicename,
isExternalCall: true,
});
return ( return (
<> <>
<Row gutter={24}> <Row gutter={24}>
@ -150,7 +156,7 @@ function External(): JSX.Element {
servicename, servicename,
selectedTraceTags, selectedTraceTags,
timestamp: selectedTimeStamp, timestamp: selectedTimeStamp,
isExternalCall: true, apmToTraceQuery,
})} })}
> >
View Traces View Traces
@ -184,7 +190,7 @@ function External(): JSX.Element {
servicename, servicename,
selectedTraceTags, selectedTraceTags,
timestamp: selectedTimeStamp, timestamp: selectedTimeStamp,
isExternalCall: true, apmToTraceQuery,
})} })}
> >
View Traces View Traces
@ -221,7 +227,7 @@ function External(): JSX.Element {
servicename, servicename,
selectedTraceTags, selectedTraceTags,
timestamp: selectedTimeStamp, timestamp: selectedTimeStamp,
isExternalCall: true, apmToTraceQuery,
})} })}
> >
View Traces View Traces
@ -255,7 +261,7 @@ function External(): JSX.Element {
servicename, servicename,
selectedTraceTags, selectedTraceTags,
timestamp: selectedTimeStamp, timestamp: selectedTimeStamp,
isExternalCall: true, apmToTraceQuery,
})} })}
> >
View Traces View Traces

View File

@ -22,6 +22,8 @@ import { useQuery } from 'react-query';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useLocation, useParams } from 'react-router-dom'; import { useLocation, useParams } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions'; import { UpdateTimeInterval } from 'store/actions';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
@ -43,6 +45,7 @@ import {
handleNonInQueryRange, handleNonInQueryRange,
onGraphClickHandler, onGraphClickHandler,
onViewTracePopupClick, onViewTracePopupClick,
useGetAPMToTracesQueries,
} from './util'; } from './util';
function Application(): JSX.Element { function Application(): JSX.Element {
@ -92,6 +95,8 @@ function Application(): JSX.Element {
convertRawQueriesToTraceSelectedTags(queries) || [], convertRawQueriesToTraceSelectedTags(queries) || [],
); );
const apmToTraceQuery = useGetAPMToTracesQueries({ servicename });
const tagFilterItems = useMemo( const tagFilterItems = useMemo(
() => () =>
handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [], handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [],
@ -159,7 +164,10 @@ function Application(): JSX.Element {
[dispatch, pathname, urlQuery], [dispatch, pathname, urlQuery],
); );
const onErrorTrackHandler = (timestamp: number): (() => void) => (): void => { const onErrorTrackHandler = (
timestamp: number,
apmToTraceQuery: Query,
): (() => void) => (): void => {
const currentTime = timestamp; const currentTime = timestamp;
const tPlusOne = timestamp + 60 * 1000; const tPlusOne = timestamp + 60 * 1000;
@ -170,15 +178,38 @@ function Application(): JSX.Element {
const avialableParams = routeConfig[ROUTES.TRACE]; const avialableParams = routeConfig[ROUTES.TRACE];
const queryString = getQueryString(avialableParams, urlParams); const queryString = getQueryString(avialableParams, urlParams);
history.replace( const JSONCompositeQuery = encodeURIComponent(
`${ JSON.stringify(apmToTraceQuery),
ROUTES.TRACE
}?selected={"serviceName":["${servicename}"],"status":["error"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&isFilterExclude={"serviceName":false,"status":false}&userSelectedFilter={"serviceName":["${servicename}"],"status":["error"]}&spanAggregateCurrentPage=1&${queryString.join(
'',
)}`,
); );
const newTraceExplorerPath = `${
ROUTES.TRACES_EXPLORER
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&${
QueryParams.compositeQuery
}=${JSONCompositeQuery}&${queryString.join('&')}`;
history.push(newTraceExplorerPath);
}; };
const errorTrackQuery = useGetAPMToTracesQueries({
servicename,
filters: [
{
id: uuid().slice(0, 8),
key: {
key: 'hasError',
dataType: DataTypes.bool,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'hasError--bool--tag--true',
},
op: 'in',
value: ['true'],
},
],
});
return ( return (
<> <>
<Row gutter={24}> <Row gutter={24}>
@ -202,6 +233,7 @@ function Application(): JSX.Element {
servicename, servicename,
selectedTraceTags, selectedTraceTags,
timestamp: selectedTimeStamp, timestamp: selectedTimeStamp,
apmToTraceQuery,
})} })}
> >
View Traces View Traces
@ -229,6 +261,7 @@ function Application(): JSX.Element {
servicename, servicename,
selectedTraceTags, selectedTraceTags,
timestamp: selectedTimeStamp, timestamp: selectedTimeStamp,
apmToTraceQuery,
})} })}
> >
View Traces View Traces
@ -245,7 +278,7 @@ function Application(): JSX.Element {
type="default" type="default"
size="small" size="small"
id="Error_button" id="Error_button"
onClick={onErrorTrackHandler(selectedTimeStamp)} onClick={onErrorTrackHandler(selectedTimeStamp, errorTrackQuery)}
> >
View Traces View Traces
</Button> </Button>

View File

@ -21,7 +21,11 @@ import { v4 as uuid } from 'uuid';
import { Button } from '../styles'; import { Button } from '../styles';
import { IServiceName } from '../types'; import { IServiceName } from '../types';
import { handleNonInQueryRange, onViewTracePopupClick } from '../util'; import {
handleNonInQueryRange,
onViewTracePopupClick,
useGetAPMToTracesQueries,
} from '../util';
function ServiceOverview({ function ServiceOverview({
onDragSelect, onDragSelect,
@ -69,6 +73,8 @@ function ServiceOverview({
const isQueryEnabled = const isQueryEnabled =
!topLevelOperationsIsLoading && topLevelOperationsRoute.length > 0; !topLevelOperationsIsLoading && topLevelOperationsRoute.length > 0;
const apmToTraceQuery = useGetAPMToTracesQueries({ servicename });
return ( return (
<> <>
<Button <Button
@ -79,6 +85,7 @@ function ServiceOverview({
servicename, servicename,
selectedTraceTags, selectedTraceTags,
timestamp: selectedTimeStamp, timestamp: selectedTimeStamp,
apmToTraceQuery,
})} })}
> >
View Traces View Traces

View File

@ -1,6 +1,10 @@
import { Tooltip, Typography } from 'antd'; import { Tooltip, Typography } from 'antd';
import { navigateToTrace } from 'container/MetricsApplication/utils'; import { navigateToTrace } from 'container/MetricsApplication/utils';
import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { v4 as uuid } from 'uuid';
import { useGetAPMToTracesQueries } from '../../util';
function ColumnWithLink({ function ColumnWithLink({
servicename, servicename,
@ -11,6 +15,25 @@ function ColumnWithLink({
}: LinkColumnProps): JSX.Element { }: LinkColumnProps): JSX.Element {
const text = record.toString(); const text = record.toString();
const apmToTraceQuery = useGetAPMToTracesQueries({
servicename,
filters: [
{
id: uuid().slice(0, 8),
key: {
key: 'name',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
},
op: 'in',
value: [text],
},
],
});
const handleOnClick = (operation: string) => (): void => { const handleOnClick = (operation: string) => (): void => {
navigateToTrace({ navigateToTrace({
servicename, servicename,
@ -18,6 +41,7 @@ function ColumnWithLink({
minTime, minTime,
maxTime, maxTime,
selectedTraceTags, selectedTraceTags,
apmToTraceQuery,
}); });
}; };

View File

@ -1,11 +1,20 @@
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { routeConfig } from 'container/SideNav/config'; import { routeConfig } from 'container/SideNav/config';
import { getQueryString } from 'container/SideNav/helper'; import { getQueryString } from 'container/SideNav/helper';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import history from 'lib/history'; import history from 'lib/history';
import { Dispatch, SetStateAction } from 'react'; import { traceFilterKeys } from 'pages/TracesExplorer/Filter/filterUtils';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; import { Dispatch, SetStateAction, useMemo } from 'react';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { Tags } from 'types/reducer/trace'; import { Tags } from 'types/reducer/trace';
import { v4 as uuid } from 'uuid';
export const dbSystemTags: Tags[] = [ export const dbSystemTags: Tags[] = [
{ {
@ -21,13 +30,13 @@ interface OnViewTracePopupClickProps {
servicename: string | undefined; servicename: string | undefined;
selectedTraceTags: string; selectedTraceTags: string;
timestamp: number; timestamp: number;
isExternalCall?: boolean; apmToTraceQuery: Query;
} }
export function onViewTracePopupClick({ export function onViewTracePopupClick({
selectedTraceTags, selectedTraceTags,
servicename, servicename,
timestamp, timestamp,
isExternalCall, apmToTraceQuery,
}: OnViewTracePopupClickProps): VoidFunction { }: OnViewTracePopupClickProps): VoidFunction {
return (): void => { return (): void => {
const currentTime = timestamp; const currentTime = timestamp;
@ -40,13 +49,17 @@ export function onViewTracePopupClick({
const avialableParams = routeConfig[ROUTES.TRACE]; const avialableParams = routeConfig[ROUTES.TRACE];
const queryString = getQueryString(avialableParams, urlParams); const queryString = getQueryString(avialableParams, urlParams);
history.replace( const JSONCompositeQuery = encodeURIComponent(
`${ JSON.stringify(apmToTraceQuery),
ROUTES.TRACE
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&spanAggregateCurrentPage=1${
isExternalCall ? '&spanKind=3' : ''
}&${queryString.join('&')}`,
); );
const newTraceExplorerPath = `${
ROUTES.TRACES_EXPLORER
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&${
QueryParams.compositeQuery
}=${JSONCompositeQuery}&${queryString.join('&')}`;
history.push(newTraceExplorerPath);
}; };
} }
@ -87,3 +100,104 @@ export const handleNonInQueryRange = (tags: TagFilterItem[]): TagFilterItem[] =>
} }
return tag; return tag;
}); });
export function handleQueryChange(
query: Query,
attributeKeys: BaseAutocompleteData,
serviceAttribute: string,
filters?: TagFilterItem[],
): Query {
const filterItem: TagFilterItem[] = [
{
id: uuid().slice(0, 8),
key: attributeKeys,
op: 'in',
value: serviceAttribute,
},
];
return {
...query,
builder: {
...query.builder,
queryData: query.builder.queryData?.map((item) => ({
...item,
filters: {
...item.filters,
items: [...item.filters.items, ...filterItem, ...(filters || [])],
},
})),
},
};
}
export function useGetAPMToTracesQueries({
servicename,
isExternalCall,
isDBCall,
filters,
}: {
servicename: string;
isExternalCall?: boolean;
isDBCall?: boolean;
filters?: TagFilterItem[];
}): Query {
const { updateAllQueriesOperators } = useQueryBuilder();
const finalFilters: TagFilterItem[] = [];
let spanKindFilter: TagFilterItem;
let dbCallFilter: TagFilterItem;
if (isExternalCall) {
spanKindFilter = {
id: uuid().slice(0, 8),
key: {
key: 'spanKind',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'spanKind--string--tag--true',
},
op: '=',
value: 'Client',
};
finalFilters.push(spanKindFilter);
}
if (isDBCall) {
dbCallFilter = {
id: uuid().slice(0, 8),
key: {
key: 'dbSystem',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'dbSystem--string--tag--true',
},
op: 'exists',
value: '',
};
finalFilters.push(dbCallFilter);
}
if (filters?.length) {
finalFilters.push(...filters);
}
return useMemo(() => {
const updatedQuery = updateAllQueriesOperators(
initialQueriesMap.traces,
PANEL_TYPES.TRACE,
DataSource.TRACES,
);
return handleQueryChange(
updatedQuery,
traceFilterKeys.serviceName,
servicename,
finalFilters,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [servicename, updateAllQueriesOperators]);
}

View File

@ -12,9 +12,13 @@ import { useRef } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuid } from 'uuid';
import { IServiceName } from './Tabs/types'; import { IServiceName } from './Tabs/types';
import { useGetAPMToTracesQueries } from './Tabs/util';
import { import {
convertedTracesToDownloadData, convertedTracesToDownloadData,
getErrorRate, getErrorRate,
@ -38,18 +42,49 @@ function TopOperationsTable({
convertRawQueriesToTraceSelectedTags(queries) || [], convertRawQueriesToTraceSelectedTags(queries) || [],
); );
const apmToTraceQuery = useGetAPMToTracesQueries({ servicename });
const params = useParams<{ servicename: string }>(); const params = useParams<{ servicename: string }>();
const handleOnClick = (operation: string): void => { const handleOnClick = (operation: string): void => {
const { servicename: encodedServiceName } = params; const { servicename: encodedServiceName } = params;
const servicename = decodeURIComponent(encodedServiceName); const servicename = decodeURIComponent(encodedServiceName);
const opFilter: TagFilterItem = {
id: uuid().slice(0, 8),
key: {
key: 'name',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
},
op: 'in',
value: [operation],
};
const preparedQuery: Query = {
...apmToTraceQuery,
builder: {
...apmToTraceQuery.builder,
queryData: apmToTraceQuery.builder.queryData?.map((item) => ({
...item,
filters: {
...item.filters,
items: [...item.filters.items, opFilter],
},
})),
},
};
navigateToTrace({ navigateToTrace({
servicename, servicename,
operation, operation,
minTime, minTime,
maxTime, maxTime,
selectedTraceTags, selectedTraceTags,
apmToTraceQuery: preparedQuery,
}); });
}; };

View File

@ -1,6 +1,6 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Widgets } from 'types/api/dashboard/getAll'; import { Widgets } from 'types/api/dashboard/getAll';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { IServiceName } from './Tabs/types'; import { IServiceName } from './Tabs/types';
@ -19,6 +19,7 @@ export interface NavigateToTraceProps {
minTime: number; minTime: number;
maxTime: number; maxTime: number;
selectedTraceTags: string; selectedTraceTags: string;
apmToTraceQuery: Query;
} }
export interface DatabaseCallsRPSProps extends DatabaseCallProps { export interface DatabaseCallsRPSProps extends DatabaseCallProps {

View File

@ -18,15 +18,21 @@ export const navigateToTrace = ({
minTime, minTime,
maxTime, maxTime,
selectedTraceTags, selectedTraceTags,
apmToTraceQuery,
}: NavigateToTraceProps): void => { }: NavigateToTraceProps): void => {
const urlParams = new URLSearchParams(); const urlParams = new URLSearchParams();
urlParams.set(QueryParams.startTime, (minTime / 1000000).toString()); urlParams.set(QueryParams.startTime, (minTime / 1000000).toString());
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString()); urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
history.push(
`${ const JSONCompositeQuery = encodeURIComponent(JSON.stringify(apmToTraceQuery));
ROUTES.TRACE
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"operation":["${operation}"]}&filterToFetchData=["duration","status","serviceName","operation"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false,"operation":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"],"operation":["${operation}"]}&spanAggregateCurrentPage=1`, const newTraceExplorerPath = `${
); ROUTES.TRACES_EXPLORER
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"operation":["${operation}"]}&filterToFetchData=["duration","status","serviceName","operation"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&${
QueryParams.compositeQuery
}=${JSONCompositeQuery}`;
history.push(newTraceExplorerPath);
}; };
export const getNearestHighestBucketValue = ( export const getNearestHighestBucketValue = (

View File

@ -53,53 +53,59 @@
padding: 0px; padding: 0px;
} }
.dashboard-breadcrumbs { .dashboard-header {
height: 48px;
padding: 16px;
border-bottom: 1px solid var(--bg-slate-400); border-bottom: 1px solid var(--bg-slate-400);
display: flex; display: flex;
gap: 6px; justify-content: space-between;
align-items: center; align-items: center;
.dashboard-btn { .dashboard-breadcrumbs {
height: 48px;
padding: 16px;
display: flex; display: flex;
gap: 6px;
align-items: center; align-items: center;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
padding: 0px;
height: 20px;
}
.dashboard-btn:hover { .dashboard-btn {
background-color: unset; display: flex;
} align-items: center;
color: var(--bg-vanilla-400);
.id-btn { font-family: Inter;
display: flex; font-size: 14px;
align-items: center; font-style: normal;
padding: 0px 2px; font-weight: 400;
border-radius: 2px; line-height: 20px; /* 142.857% */
background: rgba(113, 144, 249, 0.1); letter-spacing: -0.07px;
color: var(--bg-robin-400); padding: 0px;
font-family: Inter; height: 20px;
font-size: 14px; }
font-style: normal;
font-weight: 400; .dashboard-btn:hover {
line-height: 20px; /* 142.857% */ background-color: unset;
height: 20px; }
.ant-btn-icon { .id-btn {
margin-inline-end: 4px; display: flex;
align-items: center;
padding: 0px 2px;
border-radius: 2px;
background: rgba(113, 144, 249, 0.1);
color: var(--bg-robin-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
height: 20px;
.ant-btn-icon {
margin-inline-end: 4px;
}
}
.id-btn:hover {
background: rgba(113, 144, 249, 0.1);
color: var(--bg-robin-300);
} }
}
.id-btn:hover {
background: rgba(113, 144, 249, 0.1);
color: var(--bg-robin-300);
} }
} }
@ -562,11 +568,12 @@
.dashboard-description-container { .dashboard-description-container {
color: var(--bg-ink-400); color: var(--bg-ink-400);
.dashboard-breadcrumbs { .dashboard-header {
border-bottom: 1px solid var(--bg-vanilla-300); border-bottom: 1px solid var(--bg-vanilla-300);
.dashboard-breadcrumbs {
.dashboard-btn { .dashboard-btn {
color: var(--bg-ink-400); color: var(--bg-ink-400);
}
} }
} }

View File

@ -1,19 +1,11 @@
import './Description.styles.scss'; import './Description.styles.scss';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { import { Button, Card, Input, Modal, Popover, Tag, Typography } from 'antd';
Button,
Card,
Flex,
Input,
Modal,
Popover,
Tag,
Typography,
} from 'antd';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn'; import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { dashboardHelpMessage } from 'components/facingIssueBtn/util'; import { dashboardHelpMessage } from 'components/facingIssueBtn/util';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { DeleteButton } from 'container/ListOfDashboard/TableComponents/DeleteButton'; import { DeleteButton } from 'container/ListOfDashboard/TableComponents/DeleteButton';
@ -21,6 +13,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history'; import history from 'lib/history';
import { isEmpty } from 'lodash-es'; import { isEmpty } from 'lodash-es';
import { import {
@ -70,6 +63,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
layouts, layouts,
setLayouts, setLayouts,
isDashboardLocked, isDashboardLocked,
listSortOrder,
setSelectedDashboard, setSelectedDashboard,
handleToggleDashboardSlider, handleToggleDashboardSlider,
handleDashboardLockToggle, handleDashboardLockToggle,
@ -91,6 +85,8 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const updateDashboardMutation = useUpdateDashboard(); const updateDashboardMutation = useUpdateDashboard();
const urlQuery = useUrlQuery();
const { featureResponse, user, role } = useSelector<AppState, AppReducer>( const { featureResponse, user, role } = useSelector<AppState, AppReducer>(
(state) => state.app, (state) => state.app,
); );
@ -259,15 +255,25 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
}); });
} }
function goToListPage(): void {
urlQuery.set('columnKey', listSortOrder.columnKey as string);
urlQuery.set('order', listSortOrder.order as string);
urlQuery.set('page', listSortOrder.pagination as string);
urlQuery.delete(QueryParams.relativeTime);
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlQuery.toString()}`;
history.replace(generatedUrl);
}
return ( return (
<Card className="dashboard-description-container"> <Card className="dashboard-description-container">
<Flex justify="space-between" align="center"> <div className="dashboard-header">
<section className="dashboard-breadcrumbs"> <section className="dashboard-breadcrumbs">
<Button <Button
type="text" type="text"
icon={<LayoutGrid size={14} />} icon={<LayoutGrid size={14} />}
className="dashboard-btn" className="dashboard-btn"
onClick={(): void => history.push(ROUTES.ALL_DASHBOARD)} onClick={(): void => goToListPage()}
> >
Dashboard / Dashboard /
</Button> </Button>
@ -297,7 +303,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
buttonText="Facing issues with dashboards?" buttonText="Facing issues with dashboards?"
onHoverText="Click here to get help with dashboard details" onHoverText="Click here to get help with dashboard details"
/> />
</Flex> </div>
<section className="dashbord-details"> <section className="dashbord-details">
<div className="left-section"> <div className="left-section">
<img <img

View File

@ -71,24 +71,24 @@ function GeneralDashboardSettings(): JSX.Element {
useEffect(() => { useEffect(() => {
let numberOfUnsavedChanges = 0; let numberOfUnsavedChanges = 0;
if (!isEqual(updatedTitle, selectedData?.title)) { const initialValues = [title, description, tags, image];
numberOfUnsavedChanges += 1; const updatedValues = [
} updatedTitle,
if (!isEqual(updatedDescription, selectedData?.description)) { updatedDescription,
numberOfUnsavedChanges += 1; updatedTags,
} updatedImage,
if (!isEqual(updatedTags, selectedData?.tags)) { ];
numberOfUnsavedChanges += 1; initialValues.forEach((val, index) => {
} if (!isEqual(val, updatedValues[index])) {
if (!isEqual(updatedImage, selectedData?.image)) { numberOfUnsavedChanges += 1;
numberOfUnsavedChanges += 1; }
} });
setNumberOfUnsavedChanges(numberOfUnsavedChanges); setNumberOfUnsavedChanges(numberOfUnsavedChanges);
}, [ }, [
selectedData?.description, description,
selectedData?.image, image,
selectedData?.tags, tags,
selectedData?.title, title,
updatedDescription, updatedDescription,
updatedImage, updatedImage,
updatedTags, updatedTags,
@ -167,7 +167,8 @@ function GeneralDashboardSettings(): JSX.Element {
<div className="unsaved"> <div className="unsaved">
<div className="unsaved-dot" /> <div className="unsaved-dot" />
<Typography.Text className="unsaved-changes"> <Typography.Text className="unsaved-changes">
{numberOfUnsavedChanges} Unsaved change {numberOfUnsavedChanges} unsaved change
{numberOfUnsavedChanges > 1 && 's'}
</Typography.Text> </Typography.Text>
</div> </div>
<div className="footer-action-btns"> <div className="footer-action-btns">

View File

@ -40,12 +40,46 @@
} }
.variable-select { .variable-select {
.ant-select-dropdown { .ant-select-item {
max-width: 300px; display: flex;
align-items: center;
}
.all-label {
display: flex;
gap: 16px;
}
.dropdown-checkbox-label {
display: grid;
grid-template-columns: 24px 1fr;
}
.dropdown-value {
display: flex;
justify-content: space-between;
align-items: center;
.option-text {
max-width: 180px;
padding: 0 8px;
}
.toggle-tag-label {
padding-left: 8px;
right: 40px;
font-weight: normal;
position: absolute;
}
} }
} }
} }
.dropdown-styles {
min-width: 300px;
max-width: 350px;
}
.lightMode { .lightMode {
.variable-item { .variable-item {
.variable-name { .variable-name {

View File

@ -138,6 +138,7 @@ function DashboardVariableSelection(): JSX.Element | null {
}} }}
onValueUpdate={onValueUpdate} onValueUpdate={onValueUpdate}
variablesToGetUpdated={variablesToGetUpdated} variablesToGetUpdated={variablesToGetUpdated}
setVariablesToGetUpdated={setVariablesToGetUpdated}
/> />
))} ))}
</Row> </Row>

View File

@ -54,6 +54,7 @@ describe('VariableItem', () => {
existingVariables={{}} existingVariables={{}}
onValueUpdate={mockOnValueUpdate} onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]} variablesToGetUpdated={[]}
setVariablesToGetUpdated={(): void => {}}
/> />
</MockQueryClientProvider>, </MockQueryClientProvider>,
); );
@ -69,6 +70,7 @@ describe('VariableItem', () => {
existingVariables={{}} existingVariables={{}}
onValueUpdate={mockOnValueUpdate} onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]} variablesToGetUpdated={[]}
setVariablesToGetUpdated={(): void => {}}
/> />
</MockQueryClientProvider>, </MockQueryClientProvider>,
); );
@ -83,6 +85,7 @@ describe('VariableItem', () => {
existingVariables={{}} existingVariables={{}}
onValueUpdate={mockOnValueUpdate} onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]} variablesToGetUpdated={[]}
setVariablesToGetUpdated={(): void => {}}
/> />
</MockQueryClientProvider>, </MockQueryClientProvider>,
); );
@ -111,6 +114,7 @@ describe('VariableItem', () => {
existingVariables={{}} existingVariables={{}}
onValueUpdate={mockOnValueUpdate} onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]} variablesToGetUpdated={[]}
setVariablesToGetUpdated={(): void => {}}
/> />
</MockQueryClientProvider>, </MockQueryClientProvider>,
); );
@ -123,6 +127,8 @@ describe('VariableItem', () => {
const customVariableData = { const customVariableData = {
...mockCustomVariableData, ...mockCustomVariableData,
allSelected: true, allSelected: true,
showALLOption: true,
multiSelect: true,
}; };
render( render(
@ -132,6 +138,7 @@ describe('VariableItem', () => {
existingVariables={{}} existingVariables={{}}
onValueUpdate={mockOnValueUpdate} onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]} variablesToGetUpdated={[]}
setVariablesToGetUpdated={(): void => {}}
/> />
</MockQueryClientProvider>, </MockQueryClientProvider>,
); );
@ -147,6 +154,7 @@ describe('VariableItem', () => {
existingVariables={{}} existingVariables={{}}
onValueUpdate={mockOnValueUpdate} onValueUpdate={mockOnValueUpdate}
variablesToGetUpdated={[]} variablesToGetUpdated={[]}
setVariablesToGetUpdated={(): void => {}}
/> />
</MockQueryClientProvider>, </MockQueryClientProvider>,
); );

View File

@ -1,15 +1,29 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable no-nested-ternary */
import './DashboardVariableSelection.styles.scss'; import './DashboardVariableSelection.styles.scss';
import { orange } from '@ant-design/colors'; import { orange } from '@ant-design/colors';
import { WarningOutlined } from '@ant-design/icons'; import { WarningOutlined } from '@ant-design/icons';
import { Input, Popover, Select, Typography } from 'antd'; import {
Checkbox,
Input,
Popover,
Select,
Tag,
Tooltip,
Typography,
} from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery'; import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser'; import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
import sortValues from 'lib/dashbaordVariables/sortVariableValues'; import sortValues from 'lib/dashbaordVariables/sortVariableValues';
import { debounce, isArray, isString } from 'lodash-es'; import { debounce, isArray, isString } from 'lodash-es';
import map from 'lodash-es/map'; import map from 'lodash-es/map';
import { memo, useEffect, useMemo, useState } from 'react'; import { ChangeEvent, memo, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { IDashboardVariable } from 'types/api/dashboard/getAll'; import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { VariableResponseProps } from 'types/api/dashboard/variables/query'; import { VariableResponseProps } from 'types/api/dashboard/variables/query';
@ -23,6 +37,11 @@ const ALL_SELECT_VALUE = '__ALL__';
const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g; const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g;
enum ToggleTagValue {
Only = 'Only',
All = 'All',
}
interface VariableItemProps { interface VariableItemProps {
variableData: IDashboardVariable; variableData: IDashboardVariable;
existingVariables: Record<string, IDashboardVariable>; existingVariables: Record<string, IDashboardVariable>;
@ -33,12 +52,17 @@ interface VariableItemProps {
allSelected: boolean, allSelected: boolean,
) => void; ) => void;
variablesToGetUpdated: string[]; variablesToGetUpdated: string[];
setVariablesToGetUpdated: React.Dispatch<React.SetStateAction<string[]>>;
} }
const getSelectValue = ( const getSelectValue = (
selectedValue: IDashboardVariable['selectedValue'], selectedValue: IDashboardVariable['selectedValue'],
variableData: IDashboardVariable,
): string | string[] => { ): string | string[] => {
if (Array.isArray(selectedValue)) { if (Array.isArray(selectedValue)) {
if (!variableData.multiSelect && selectedValue.length === 1) {
return selectedValue[0]?.toString() || '';
}
return selectedValue.map((item) => item.toString()); return selectedValue.map((item) => item.toString());
} }
return selectedValue?.toString() || ''; return selectedValue?.toString() || '';
@ -50,6 +74,7 @@ function VariableItem({
existingVariables, existingVariables,
onValueUpdate, onValueUpdate,
variablesToGetUpdated, variablesToGetUpdated,
setVariablesToGetUpdated,
}: VariableItemProps): JSX.Element { }: VariableItemProps): JSX.Element {
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>( const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
[], [],
@ -148,6 +173,10 @@ function VariableItem({
} }
setOptionsData(newOptionsData); setOptionsData(newOptionsData);
} else {
setVariablesToGetUpdated((prev) =>
prev.filter((name) => name !== variableData.name),
);
} }
} }
} catch (e) { } catch (e) {
@ -193,7 +222,7 @@ function VariableItem({
}); });
const handleChange = (value: string | string[]): void => { const handleChange = (value: string | string[]): void => {
if (variableData.name) if (variableData.name) {
if ( if (
value === ALL_SELECT_VALUE || value === ALL_SELECT_VALUE ||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) || (Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
@ -203,25 +232,29 @@ function VariableItem({
} else { } else {
onValueUpdate(variableData.name, variableData.id, value, false); onValueUpdate(variableData.name, variableData.id, value, false);
} }
}
}; };
// do not debounce the above function as we do not need debounce in select variables // do not debounce the above function as we do not need debounce in select variables
const debouncedHandleChange = debounce(handleChange, 500); const debouncedHandleChange = debounce(handleChange, 500);
const { selectedValue } = variableData; const { selectedValue } = variableData;
const selectedValueStringified = useMemo(() => getSelectValue(selectedValue), [ const selectedValueStringified = useMemo(
selectedValue, () => getSelectValue(selectedValue, variableData),
]); [selectedValue, variableData],
);
const selectValue = variableData.allSelected const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
? 'ALL'
: selectedValueStringified;
const mode = const selectValue =
variableData.allSelected && enableSelectAll
? 'ALL'
: selectedValueStringified;
const mode: 'multiple' | undefined =
variableData.multiSelect && !variableData.allSelected variableData.multiSelect && !variableData.allSelected
? 'multiple' ? 'multiple'
: undefined; : undefined;
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
useEffect(() => { useEffect(() => {
// Fetch options for CUSTOM Type // Fetch options for CUSTOM Type
@ -231,6 +264,117 @@ function VariableItem({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [variableData.type, variableData.customValue]); }, [variableData.type, variableData.customValue]);
const checkAll = (e: MouseEvent): void => {
e.stopPropagation();
e.preventDefault();
const isChecked =
variableData.allSelected || selectValue.includes(ALL_SELECT_VALUE);
if (isChecked) {
handleChange([]);
} else {
handleChange(ALL_SELECT_VALUE);
}
};
const handleOptionSelect = (
e: CheckboxChangeEvent,
option: string | number | boolean,
): void => {
const newSelectedValue = Array.isArray(selectedValue)
? ((selectedValue.filter(
(val) => val.toString() !== option.toString(),
) as unknown) as string[])
: [];
if (
!e.target.checked &&
Array.isArray(selectedValueStringified) &&
selectedValueStringified.includes(option.toString())
) {
if (newSelectedValue.length === 0) {
handleChange(ALL_SELECT_VALUE);
return;
}
if (newSelectedValue.length === 1) {
handleChange(newSelectedValue[0].toString());
return;
}
handleChange(newSelectedValue);
} else if (!e.target.checked && selectedValue === option.toString()) {
handleChange(ALL_SELECT_VALUE);
} else if (newSelectedValue.length === optionsData.length - 1) {
handleChange(ALL_SELECT_VALUE);
}
};
const [optionState, setOptionState] = useState({
tag: '',
visible: false,
});
function currentToggleTagValue({
option,
}: {
option: string;
}): ToggleTagValue {
if (
option.toString() === selectValue ||
(Array.isArray(selectValue) &&
selectValue?.includes(option.toString()) &&
selectValue.length === 1)
) {
return ToggleTagValue.All;
}
return ToggleTagValue.Only;
}
function handleToggle(e: ChangeEvent, option: string): void {
e.stopPropagation();
const mode = currentToggleTagValue({ option: option as string });
const isChecked =
variableData.allSelected ||
option.toString() === selectValue ||
(Array.isArray(selectValue) && selectValue?.includes(option.toString()));
if (isChecked) {
if (mode === ToggleTagValue.Only) {
handleChange(option.toString());
} else if (!variableData.multiSelect) {
handleChange(option.toString());
} else {
handleChange(ALL_SELECT_VALUE);
}
} else {
handleChange(option.toString());
}
}
function retProps(
option: string,
): {
onMouseOver: () => void;
onMouseOut: () => void;
} {
return {
onMouseOver: (): void =>
setOptionState({
tag: option.toString(),
visible: true,
}),
onMouseOut: (): void =>
setOptionState({
tag: option.toString(),
visible: false,
}),
};
}
const ensureValidOption = (option: string): boolean =>
!(
currentToggleTagValue({ option }) === ToggleTagValue.All && !enableSelectAll
);
return ( return (
<div className="variable-item"> <div className="variable-item">
<Typography.Text className="variable-name" ellipsis> <Typography.Text className="variable-name" ellipsis>
@ -264,19 +408,35 @@ function VariableItem({
onChange={handleChange} onChange={handleChange}
bordered={false} bordered={false}
placeholder="Select value" placeholder="Select value"
placement="bottomRight" placement="bottomLeft"
mode={mode} mode={mode}
dropdownMatchSelectWidth={false}
style={SelectItemStyle} style={SelectItemStyle}
loading={isLoading} loading={isLoading}
showSearch showSearch
data-testid="variable-select" data-testid="variable-select"
className="variable-select" className="variable-select"
popupClassName="dropdown-styles"
maxTagCount={4}
getPopupContainer={popupContainer} getPopupContainer={popupContainer}
// eslint-disable-next-line react/no-unstable-nested-components
tagRender={(props): JSX.Element => (
<Tag closable onClose={props.onClose}>
{props.value}
</Tag>
)}
// eslint-disable-next-line react/no-unstable-nested-components
maxTagPlaceholder={(omittedValues): JSX.Element => (
<Tooltip title={omittedValues.map(({ value }) => value).join(', ')}>
<span>+ {omittedValues.length} </span>
</Tooltip>
)}
> >
{enableSelectAll && ( {enableSelectAll && (
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}> <Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
ALL <div className="all-label" onClick={(e): void => checkAll(e as any)}>
<Checkbox checked={variableData.allSelected} />
ALL
</div>
</Select.Option> </Select.Option>
)} )}
{map(optionsData, (option) => ( {map(optionsData, (option) => (
@ -285,7 +445,45 @@ function VariableItem({
key={option.toString()} key={option.toString()}
value={option} value={option}
> >
{option.toString()} <div
className={variableData.multiSelect ? 'dropdown-checkbox-label' : ''}
>
{variableData.multiSelect && (
<Checkbox
onChange={(e): void => {
e.stopPropagation();
e.preventDefault();
handleOptionSelect(e, option);
}}
checked={
variableData.allSelected ||
option.toString() === selectValue ||
(Array.isArray(selectValue) &&
selectValue?.includes(option.toString()))
}
/>
)}
<div
className="dropdown-value"
{...retProps(option as string)}
onClick={(e): void => handleToggle(e as any, option as string)}
>
<Tooltip title={option.toString()} placement="bottomRight">
<Typography.Text ellipsis className="option-text">
{option.toString()}
</Typography.Text>
</Tooltip>
{variableData.multiSelect &&
optionState.tag === option.toString() &&
optionState.visible &&
ensureValidOption(option as string) && (
<Typography.Text className="toggle-tag-label">
{currentToggleTagValue({ option: option as string })}
</Typography.Text>
)}
</div>
</div>
</Select.Option> </Select.Option>
))} ))}
</Select> </Select>

View File

@ -42,4 +42,5 @@ export const VariableValue = styled(Typography)`
export const SelectItemStyle = { export const SelectItemStyle = {
minWidth: 120, minWidth: 120,
fontSize: '0.8rem', fontSize: '0.8rem',
width: '100%',
}; };

View File

@ -12,7 +12,6 @@ import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interface
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { defaultTo } from 'lodash-es'; import { defaultTo } from 'lodash-es';
@ -33,7 +32,6 @@ import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import { GlobalReducer } from 'types/reducer/globalTime';
import ClickHouseQueryContainer from './QueryBuilder/clickHouse'; import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
import PromQLQueryContainer from './QueryBuilder/promQL'; import PromQLQueryContainer from './QueryBuilder/promQL';
@ -46,10 +44,6 @@ function QuerySection({
const urlQuery = useUrlQuery(); const urlQuery = useUrlQuery();
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys(); const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { featureResponse } = useSelector<AppState, AppReducer>( const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app, (state) => state.app,
); );
@ -80,8 +74,6 @@ function QuerySection({
return; return;
} }
const updatedQuery = updateStepInterval(query, maxTime, minTime);
const selectedWidgetIndex = getSelectedWidgetIndex( const selectedWidgetIndex = getSelectedWidgetIndex(
selectedDashboard, selectedDashboard,
selectedWidget.id, selectedWidget.id,
@ -102,18 +94,16 @@ function QuerySection({
...previousWidgets, ...previousWidgets,
{ {
...selectedWidget, ...selectedWidget,
query: updatedQuery, query,
}, },
...nextWidgets, ...nextWidgets,
], ],
}, },
}); });
redirectWithQueryBuilderData(updatedQuery); redirectWithQueryBuilderData(query);
}, },
[ [
selectedDashboard, selectedDashboard,
maxTime,
minTime,
selectedWidget, selectedWidget,
setSelectedDashboard, setSelectedDashboard,
redirectWithQueryBuilderData, redirectWithQueryBuilderData,
@ -137,7 +127,7 @@ function QuerySection({
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => { const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const config: QueryBuilderProps['filterConfigs'] = { const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: false, isDisabled: true }, stepInterval: { isHidden: false, isDisabled: false },
}; };
return config; return config;

View File

@ -72,10 +72,16 @@ function LeftContainer({
globalSelectedInterval, globalSelectedInterval,
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes), graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
query: stagedQuery, query: stagedQuery,
fillGaps: selectedWidget.fillSpans || false,
})); }));
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [stagedQuery, selectedTime, globalSelectedInterval]); }, [
stagedQuery,
selectedTime,
selectedWidget.fillSpans,
globalSelectedInterval,
]);
const queryResponse = useGetQueryRange( const queryResponse = useGetQueryRange(
requestData, requestData,

View File

@ -4,6 +4,7 @@ import { Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Dispatch, SetStateAction } from 'react'; import { Dispatch, SetStateAction } from 'react';
import { ColumnUnit } from 'types/api/dashboard/getAll'; import { ColumnUnit } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
import YAxisUnitSelector from '../YAxisUnitSelector'; import YAxisUnitSelector from '../YAxisUnitSelector';
@ -18,7 +19,13 @@ export function ColumnUnitSelector(
const { currentQuery } = useQueryBuilder(); const { currentQuery } = useQueryBuilder();
function getAggregateColumnsNamesAndLabels(): string[] { function getAggregateColumnsNamesAndLabels(): string[] {
return currentQuery.builder.queryData.map((q) => q.queryName); if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
return currentQuery.builder.queryData.map((q) => q.queryName);
}
if (currentQuery.queryType === EQueryType.CLICKHOUSE) {
return currentQuery.clickhouse_sql.map((q) => q.name);
}
return currentQuery.promql.map((q) => q.name);
} }
const { columnUnits, setColumnUnits } = props; const { columnUnits, setColumnUnits } = props;

View File

@ -241,6 +241,24 @@
} }
} }
.stack-chart {
margin-top: 16px;
display: flex;
justify-content: space-between;
gap: 8px;
.label {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
letter-spacing: 0.52px;
text-transform: uppercase;
}
}
.bucket-config { .bucket-config {
margin-top: 16px; margin-top: 16px;
display: flex; display: flex;
@ -411,6 +429,21 @@
} }
} }
.bucket-config {
.label {
color: var(--bg-ink-400);
}
.bucket-input {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-300);
.ant-input {
background: var(--bg-vanilla-300);
}
}
}
.panel-time-text { .panel-time-text {
color: var(--bg-ink-400); color: var(--bg-ink-400);
} }

View File

@ -132,3 +132,17 @@ export const panelTypeVsColumnUnitPreferences: {
[PANEL_TYPES.HISTOGRAM]: false, [PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false, [PANEL_TYPES.EMPTY_WIDGET]: false,
} as const; } as const;
export const panelTypeVsStackingChartPreferences: {
[key in PANEL_TYPES]: boolean;
} = {
[PANEL_TYPES.TIME_SERIES]: false,
[PANEL_TYPES.VALUE]: false,
[PANEL_TYPES.TABLE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;

View File

@ -29,6 +29,7 @@ import {
panelTypeVsFillSpan, panelTypeVsFillSpan,
panelTypeVsPanelTimePreferences, panelTypeVsPanelTimePreferences,
panelTypeVsSoftMinMax, panelTypeVsSoftMinMax,
panelTypeVsStackingChartPreferences,
panelTypeVsThreshold, panelTypeVsThreshold,
panelTypeVsYAxisUnit, panelTypeVsYAxisUnit,
} from './constants'; } from './constants';
@ -48,6 +49,8 @@ function RightContainer({
selectedGraph, selectedGraph,
bucketCount, bucketCount,
bucketWidth, bucketWidth,
stackedBarChart,
setStackedBarChart,
setBucketCount, setBucketCount,
setBucketWidth, setBucketWidth,
setSelectedTime, setSelectedTime,
@ -87,6 +90,8 @@ function RightContainer({
const allowYAxisUnit = panelTypeVsYAxisUnit[selectedGraph]; const allowYAxisUnit = panelTypeVsYAxisUnit[selectedGraph];
const allowCreateAlerts = panelTypeVsCreateAlert[selectedGraph]; const allowCreateAlerts = panelTypeVsCreateAlert[selectedGraph];
const allowBucketConfig = panelTypeVsBucketConfig[selectedGraph]; const allowBucketConfig = panelTypeVsBucketConfig[selectedGraph];
const allowStackingBarChart =
panelTypeVsStackingChartPreferences[selectedGraph];
const allowPanelTimePreference = const allowPanelTimePreference =
panelTypeVsPanelTimePreferences[selectedGraph]; panelTypeVsPanelTimePreferences[selectedGraph];
@ -231,6 +236,17 @@ function RightContainer({
</section> </section>
)} )}
{allowStackingBarChart && (
<section className="stack-chart">
<Typography.Text className="label">Stack series</Typography.Text>
<Switch
checked={stackedBarChart}
size="small"
onChange={(checked): void => setStackedBarChart(checked)}
/>
</section>
)}
{allowBucketConfig && ( {allowBucketConfig && (
<section className="bucket-config"> <section className="bucket-config">
<Typography.Text className="label">Number of buckets</Typography.Text> <Typography.Text className="label">Number of buckets</Typography.Text>
@ -312,6 +328,8 @@ interface RightContainerProps {
setSelectedTime: Dispatch<SetStateAction<timePreferance>>; setSelectedTime: Dispatch<SetStateAction<timePreferance>>;
selectedTime: timePreferance; selectedTime: timePreferance;
yAxisUnit: string; yAxisUnit: string;
stackedBarChart: boolean;
setStackedBarChart: Dispatch<SetStateAction<boolean>>;
bucketWidth: number; bucketWidth: number;
bucketCount: number; bucketCount: number;
combineHistogram: boolean; combineHistogram: boolean;

View File

@ -126,6 +126,10 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const [stacked, setStacked] = useState<boolean>( const [stacked, setStacked] = useState<boolean>(
selectedWidget?.isStacked || false, selectedWidget?.isStacked || false,
); );
const [stackedBarChart, setStackedBarChart] = useState<boolean>(
selectedWidget?.stackedBarChart || false,
);
const [opacity, setOpacity] = useState<string>(selectedWidget?.opacity || '1'); const [opacity, setOpacity] = useState<string>(selectedWidget?.opacity || '1');
const [thresholds, setThresholds] = useState<ThresholdProps[]>( const [thresholds, setThresholds] = useState<ThresholdProps[]>(
selectedWidget?.thresholds || [], selectedWidget?.thresholds || [],
@ -195,6 +199,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
fillSpans: isFillSpans, fillSpans: isFillSpans,
columnUnits, columnUnits,
bucketCount, bucketCount,
stackedBarChart,
bucketWidth, bucketWidth,
mergeAllActiveQueries: combineHistogram, mergeAllActiveQueries: combineHistogram,
selectedLogFields, selectedLogFields,
@ -219,6 +224,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
bucketWidth, bucketWidth,
bucketCount, bucketCount,
combineHistogram, combineHistogram,
stackedBarChart,
]); ]);
const closeModal = (): void => { const closeModal = (): void => {
@ -307,6 +313,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
opacity: selectedWidget?.opacity || '1', opacity: selectedWidget?.opacity || '1',
nullZeroValues: selectedWidget?.nullZeroValues || 'zero', nullZeroValues: selectedWidget?.nullZeroValues || 'zero',
title: selectedWidget?.title, title: selectedWidget?.title,
stackedBarChart: selectedWidget?.stackedBarChart || false,
yAxisUnit: selectedWidget?.yAxisUnit, yAxisUnit: selectedWidget?.yAxisUnit,
panelTypes: graphType, panelTypes: graphType,
query: currentQuery, query: currentQuery,
@ -332,6 +339,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
opacity: selectedWidget?.opacity || '1', opacity: selectedWidget?.opacity || '1',
nullZeroValues: selectedWidget?.nullZeroValues || 'zero', nullZeroValues: selectedWidget?.nullZeroValues || 'zero',
title: selectedWidget?.title, title: selectedWidget?.title,
stackedBarChart: selectedWidget?.stackedBarChart || false,
yAxisUnit: selectedWidget?.yAxisUnit, yAxisUnit: selectedWidget?.yAxisUnit,
panelTypes: graphType, panelTypes: graphType,
query: currentQuery, query: currentQuery,
@ -532,6 +540,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
setDescription={setDescription} setDescription={setDescription}
stacked={stacked} stacked={stacked}
setStacked={setStacked} setStacked={setStacked}
stackedBarChart={stackedBarChart}
setStackedBarChart={setStackedBarChart}
opacity={opacity} opacity={opacity}
yAxisUnit={yAxisUnit} yAxisUnit={yAxisUnit}
columnUnits={columnUnits} columnUnits={columnUnits}

View File

@ -10,10 +10,9 @@ export const Container = styled.div`
export const RightContainerWrapper = styled(Col)` export const RightContainerWrapper = styled(Col)`
&&& { &&& {
min-width: 330px;
overflow-y: auto;
max-width: 400px; max-width: 400px;
width: 30%; width: 30%;
overflow-y: auto;
} }
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 0rem; width: 0rem;
@ -26,7 +25,7 @@ interface LeftContainerWrapperProps {
export const LeftContainerWrapper = styled(Col)<LeftContainerWrapperProps>` export const LeftContainerWrapper = styled(Col)<LeftContainerWrapperProps>`
&&& { &&& {
min-width: 70%; width: 100%;
overflow-y: auto; overflow-y: auto;
border-right: ${({ isDarkMode }): string => border-right: ${({ isDarkMode }): string =>
isDarkMode isDarkMode

View File

@ -189,50 +189,6 @@ export const panelTypeDataSourceFormValuesMap: Record<
}, },
}, },
}, },
[PANEL_TYPES.HISTOGRAM]: {
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'spaceAggregation',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
],
},
},
},
[PANEL_TYPES.TABLE]: { [PANEL_TYPES.TABLE]: {
[DataSource.LOGS]: { [DataSource.LOGS]: {
builder: { builder: {

View File

@ -60,7 +60,7 @@ function PiePanelWrapper({
d.series?.length === 1 d.series?.length === 1
? getLabel(Object.values(s.labels)[0], widget.query, d.queryName) ? getLabel(Object.values(s.labels)[0], widget.query, d.queryName)
: getLabel(Object.values(s.labels)[0], {} as Query, d.queryName, true), : getLabel(Object.values(s.labels)[0], {} as Query, d.queryName, true),
themeColors.chartcolors, isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
), ),
})), })),
) )

View File

@ -0,0 +1,4 @@
.info-text {
margin-top: 8px;
padding: 8px;
}

View File

@ -1,3 +1,6 @@
import './UplotPanelWrapper.styles.scss';
import { Alert } from 'antd';
import { ToggleGraphProps } from 'components/Graph/types'; import { ToggleGraphProps } from 'components/Graph/types';
import Uplot from 'components/Uplot'; import Uplot from 'components/Uplot';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
@ -8,6 +11,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions'; import { useResizeObserver } from 'hooks/useDimensions';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { cloneDeep, isEqual, isUndefined } from 'lodash-es';
import _noop from 'lodash-es/noop'; import _noop from 'lodash-es/noop';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
@ -35,6 +39,8 @@ function UplotPanelWrapper({
const [maxTimeScale, setMaxTimeScale] = useState<number>(); const [maxTimeScale, setMaxTimeScale] = useState<number>();
const { currentQuery } = useQueryBuilder(); const { currentQuery } = useQueryBuilder();
const [hiddenGraph, setHiddenGraph] = useState<{ [key: string]: boolean }>();
useEffect(() => { useEffect(() => {
if (toScrollWidgetId === widget.id) { if (toScrollWidgetId === widget.id) {
graphRef.current?.scrollIntoView({ graphRef.current?.scrollIntoView({
@ -78,8 +84,26 @@ function UplotPanelWrapper({
const chartData = getUPlotChartData( const chartData = getUPlotChartData(
queryResponse?.data?.payload, queryResponse?.data?.payload,
widget.fillSpans, widget.fillSpans,
widget?.stackedBarChart,
hiddenGraph,
); );
useEffect(() => {
if (widget.panelTypes === PANEL_TYPES.BAR && widget?.stackedBarChart) {
const graphV = cloneDeep(graphVisibility)?.slice(1);
const isSomeSelectedLegend = graphV?.some((v) => v === false);
if (isSomeSelectedLegend) {
const hiddenIndex = graphV?.findIndex((v) => v === true);
if (!isUndefined(hiddenIndex) && hiddenIndex !== -1) {
const updatedHiddenGraph = { [hiddenIndex]: true };
if (!isEqual(hiddenGraph, updatedHiddenGraph)) {
setHiddenGraph(updatedHiddenGraph);
}
}
}
}
}, [graphVisibility, hiddenGraph, widget.panelTypes, widget?.stackedBarChart]);
const options = useMemo( const options = useMemo(
() => () =>
getUPlotChartOptions({ getUPlotChartOptions({
@ -99,6 +123,9 @@ function UplotPanelWrapper({
setGraphsVisibilityStates: setGraphVisibility, setGraphsVisibilityStates: setGraphVisibility,
panelType: selectedGraph || widget.panelTypes, panelType: selectedGraph || widget.panelTypes,
currentQuery, currentQuery,
stackBarChart: widget?.stackedBarChart,
hiddenGraph,
setHiddenGraph,
}), }),
[ [
widget?.id, widget?.id,
@ -107,6 +134,7 @@ function UplotPanelWrapper({
widget.softMax, widget.softMax,
widget.softMin, widget.softMin,
widget.panelTypes, widget.panelTypes,
widget?.stackedBarChart,
queryResponse.data?.payload, queryResponse.data?.payload,
containerDimensions, containerDimensions,
isDarkMode, isDarkMode,
@ -118,15 +146,23 @@ function UplotPanelWrapper({
setGraphVisibility, setGraphVisibility,
selectedGraph, selectedGraph,
currentQuery, currentQuery,
hiddenGraph,
], ],
); );
return ( return (
<div style={{ height: '100%', width: '100%' }} ref={graphRef}> <div style={{ height: '100%', width: '100%' }} ref={graphRef}>
<Uplot options={options} data={chartData} ref={lineChartRef} /> <Uplot options={options} data={chartData} ref={lineChartRef} />
{isFullViewMode && setGraphVisibility && ( {widget?.stackedBarChart && isFullViewMode && (
<Alert
message="Selecting multiple legends is currently not supported in case of stacked bar charts"
type="info"
className="info-text"
/>
)}
{isFullViewMode && setGraphVisibility && !widget?.stackedBarChart && (
<GraphManager <GraphManager
data={chartData} data={getUPlotChartData(queryResponse?.data?.payload, widget.fillSpans)}
name={widget.id} name={widget.id}
options={options} options={options}
yAxisUnit={widget.yAxisUnit} yAxisUnit={widget.yAxisUnit}

View File

@ -42,9 +42,11 @@ describe('PipelinePage container test', () => {
jest.advanceTimersByTime(299); jest.advanceTimersByTime(299);
expect(setPipelineValue).not.toHaveBeenCalled(); expect(setPipelineValue).not.toHaveBeenCalled();
// Wait for the debounce delay to pass // Fast-forward time by 1ms to reach the debounce delay
jest.advanceTimersByTime(1);
// Wait for the debounce delay to pass and expect the callback to be called
await waitFor(() => { await waitFor(() => {
// Expect the callback to be called after debounce delay
expect(setPipelineValue).toHaveBeenCalledWith('sample_pipeline'); expect(setPipelineValue).toHaveBeenCalledWith('sample_pipeline');
}); });

View File

@ -1,13 +1,26 @@
import '../ServiceApplication.styles.scss';
import { SearchOutlined } from '@ant-design/icons'; import { SearchOutlined } from '@ant-design/icons';
import { Popconfirm, PopconfirmProps } from 'antd';
import type { ColumnType } from 'antd/es/table'; import type { ColumnType } from 'antd/es/table';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { routeConfig } from 'container/SideNav/config'; import { routeConfig } from 'container/SideNav/config';
import { getQueryString } from 'container/SideNav/helper'; import { getQueryString } from 'container/SideNav/helper';
import history from 'lib/history';
import { Info } from 'lucide-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ServicesList } from 'types/api/metrics/getService'; import { ServicesList } from 'types/api/metrics/getService';
import { filterDropdown } from '../Filter/FilterDropdown'; import { filterDropdown } from '../Filter/FilterDropdown';
import { Name } from '../styles';
const MAX_TOP_LEVEL_OPERATIONS = 2500;
const highTopLevelOperationsPopoverDesc = (metrics: string): JSX.Element => (
<div className="popover-description">
The service `{metrics}` has too many top level operations. It makes the
dashboard slow to load.
</div>
);
export const getColumnSearchProps = ( export const getColumnSearchProps = (
dataIndex: keyof ServicesList, dataIndex: keyof ServicesList,
@ -15,24 +28,61 @@ export const getColumnSearchProps = (
): ColumnType<ServicesList> => ({ ): ColumnType<ServicesList> => ({
filterDropdown, filterDropdown,
filterIcon: <SearchOutlined />, filterIcon: <SearchOutlined />,
onFilter: (value: string | number | boolean, record: ServicesList): boolean => onFilter: (
record[dataIndex] value: string | number | boolean,
.toString() record: ServicesList,
.toLowerCase() ): boolean => {
.includes(value.toString().toLowerCase()), if (record[dataIndex]) {
render: (metrics: string): JSX.Element => { record[dataIndex]
?.toString()
.toLowerCase()
.includes(value.toString().toLowerCase());
}
return false;
},
render: (metrics: string, record: ServicesList): JSX.Element => {
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const avialableParams = routeConfig[ROUTES.SERVICE_METRICS]; const avialableParams = routeConfig[ROUTES.SERVICE_METRICS];
const queryString = getQueryString(avialableParams, urlParams); const queryString = getQueryString(avialableParams, urlParams);
const topLevelOperations = record?.dataWarning?.topLevelOps || [];
const handleShowTopLevelOperations: PopconfirmProps['onConfirm'] = () => {
history.push(
`${ROUTES.APPLICATION}/${encodeURIComponent(metrics)}/top-level-operations`,
);
};
const hasHighTopLevelOperations =
topLevelOperations &&
Array.isArray(topLevelOperations) &&
topLevelOperations.length > MAX_TOP_LEVEL_OPERATIONS;
return ( return (
<Link <div className={`serviceName ${hasHighTopLevelOperations ? 'error' : ''} `}>
to={`${ROUTES.APPLICATION}/${encodeURIComponent( {hasHighTopLevelOperations && (
metrics, <Popconfirm
)}?${queryString.join('')}`} title="Too Many Top Level Operations"
> description={highTopLevelOperationsPopoverDesc(metrics)}
<Name>{metrics}</Name> placement="right"
</Link> overlayClassName="service-high-top-level-operations"
onConfirm={handleShowTopLevelOperations}
trigger={['hover']}
showCancel={false}
okText="Show Top Level Operations"
>
<Info size={14} />
</Popconfirm>
)}
<Link
to={`${ROUTES.APPLICATION}/${encodeURIComponent(
metrics,
)}?${queryString.join('')}`}
>
{metrics}
</Link>
</div>
); );
}, },
}); });

View File

@ -0,0 +1,25 @@
.serviceName {
color: #4e74f8;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.error {
color: var(--bg-cherry-500);
a {
color: var(--bg-cherry-500);
}
}
.service-high-top-level-operations {
width: 300px;
.popover-description {
padding: 16px 0;
}
}

View File

@ -1,7 +1,7 @@
import * as Sentry from '@sentry/react';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import useFeatureFlag from 'hooks/useFeatureFlag'; import useFeatureFlag from 'hooks/useFeatureFlag';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { ErrorBoundary } from 'react-error-boundary';
import ServiceMetrics from './ServiceMetrics'; import ServiceMetrics from './ServiceMetrics';
import ServiceTraces from './ServiceTraces'; import ServiceTraces from './ServiceTraces';
@ -12,11 +12,11 @@ function Services(): JSX.Element {
?.active; ?.active;
return ( return (
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}> <Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<Container style={{ marginTop: 0 }}> <Container style={{ marginTop: 0 }}>
{isSpanMetricEnabled ? <ServiceMetrics /> : <ServiceTraces />} {isSpanMetricEnabled ? <ServiceMetrics /> : <ServiceTraces />}
</Container> </Container>
</ErrorBoundary> </Sentry.ErrorBoundary>
); );
} }

View File

@ -15,11 +15,19 @@ export const getColumnSearchProps = (
): ColumnType<ServicesList> => ({ ): ColumnType<ServicesList> => ({
filterDropdown, filterDropdown,
filterIcon: <SearchOutlined />, filterIcon: <SearchOutlined />,
onFilter: (value: string | number | boolean, record: ServicesList): boolean => onFilter: (
record[dataIndex] value: string | number | boolean,
.toString() record: ServicesList,
.toLowerCase() ): boolean => {
.includes(value.toString().toLowerCase()), if (record[dataIndex]) {
record[dataIndex]
?.toString()
.toLowerCase()
.includes(value.toString().toLowerCase());
}
return false;
},
render: (metrics: string): JSX.Element => { render: (metrics: string): JSX.Element => {
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const avialableParams = routeConfig[ROUTES.SERVICE_METRICS]; const avialableParams = routeConfig[ROUTES.SERVICE_METRICS];

View File

@ -209,7 +209,9 @@ function SideNav({
if (event && isCtrlMetaKey(event)) { if (event && isCtrlMetaKey(event)) {
openInNewTab(`${key}?${queryString.join('&')}`); openInNewTab(`${key}?${queryString.join('&')}`);
} else { } else {
history.push(`${key}?${queryString.join('&')}`); history.push(`${key}?${queryString.join('&')}`, {
from: pathname,
});
} }
} }
}, },

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