Compare commits

..

2 Commits

Author SHA1 Message Date
vikrantgupta25
b2af2a1cf4 chore(analytics): associate user events with license group 2025-06-11 23:14:56 +05:30
vikrantgupta25
d901b60aa4 chore(analytics): associate user events with license group 2025-06-11 23:09:09 +05:30
561 changed files with 9670 additions and 41192 deletions

7
.github/CODEOWNERS vendored
View File

@@ -12,9 +12,4 @@
/pkg/factory/ @grandwizard28
/pkg/types/ @grandwizard28
.golangci.yml @grandwizard28
/pkg/zeus/ @vikrantgupta25
/pkg/licensing/ @vikrantgupta25
/pkg/sqlmigration/ @vikrantgupta25
/ee/zeus/ @vikrantgupta25
/ee/licensing/ @vikrantgupta25
/ee/sqlmigration/ @vikrantgupta25
**/(zeus|licensing|sqlmigration)/ @vikrantgupta25

View File

@@ -7,7 +7,6 @@ linters:
- sloglint
- depguard
- iface
- unparam
linters-settings:
sloglint:

View File

@@ -90,15 +90,6 @@ apiserver:
- /api/v1/version
- /
##################### Querier #####################
querier:
# The TTL for cached query results.
cache_ttl: 168h
# The interval for recent data that should not be cached.
flux_interval: 5m
# The maximum number of concurrent queries for missing ranges.
max_concurrent_queries: 4
##################### TelemetryStore #####################
telemetrystore:
# Maximum number of idle connections in the connection pool.
@@ -112,15 +103,13 @@ telemetrystore:
clickhouse:
# The DSN to use for clickhouse.
dsn: tcp://localhost:9000
# The cluster name to use for clickhouse.
cluster: cluster
# The query settings for clickhouse.
settings:
max_execution_time: 0
max_execution_time_leaf: 0
timeout_before_checking_execution_speed: 0
max_bytes_to_read: 0
max_result_rows: 0
max_result_rows_for_ch_query: 0
##################### Prometheus #####################
prometheus:
@@ -235,12 +224,3 @@ statsreporter:
enabled: true
# The interval at which the stats are collected.
interval: 6h
collect:
# Whether to collect identities and traits (emails).
identities: true
##################### Gateway (License only) #####################
gateway:
# The URL of the gateway's api.
url: http://localhost:8080

View File

@@ -100,18 +100,12 @@ services:
# - "9000:9000"
# - "8123:8123"
# - "9181:9181"
configs:
- source: clickhouse-config
target: /etc/clickhouse-server/config.xml
- source: clickhouse-users
target: /etc/clickhouse-server/users.xml
- source: clickhouse-custom-function
target: /etc/clickhouse-server/custom-function.xml
- source: clickhouse-cluster
target: /etc/clickhouse-server/config.d/cluster.xml
volumes:
- ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml
- ../common/clickhouse/users.xml:/etc/clickhouse-server/users.xml
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ../common/clickhouse/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
- clickhouse:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
@@ -123,10 +117,9 @@ services:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port
volumes:
- ../common/signoz/prometheus.yml:/root/config/prometheus.yml
- ../common/dashboards:/root/config/dashboards
- sqlite:/var/lib/signoz/
configs:
- source: signoz-prometheus-config
target: /root/config/prometheus.yml
environment:
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
@@ -154,11 +147,9 @@ services:
- --manager-config=/etc/manager-config.yaml
- --copy-path=/var/tmp/collector-config.yaml
- --feature-gates=-pkg.translator.prometheus.NormalizeName
configs:
- source: otel-collector-config
target: /etc/otel-collector-config.yaml
- source: otel-manager-config
target: /etc/manager-config.yaml
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- ../common/signoz/otel-collector-opamp-config.yaml:/etc/manager-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}}
- LOW_CARDINAL_EXCEPTION_GROUPING=false
@@ -195,26 +186,3 @@ volumes:
name: signoz-sqlite
zookeeper-1:
name: signoz-zookeeper-1
configs:
clickhouse-config:
file: ../common/clickhouse/config.xml
clickhouse-users:
file: ../common/clickhouse/users.xml
clickhouse-custom-function:
file: ../common/clickhouse/custom-function.xml
clickhouse-cluster:
file: ../common/clickhouse/cluster.xml
signoz-prometheus-config:
file: ../common/signoz/prometheus.yml
# If you have multiple dashboard files, you can list them individually:
# dashboard-foo:
# file: ../common/dashboards/foo.json
# dashboard-bar:
# file: ../common/dashboards/bar.json
otel-collector-config:
file: ./otel-collector-config.yaml
otel-manager-config:
file: ../common/signoz/otel-collector-opamp-config.yaml

View File

@@ -6,13 +6,11 @@ import (
"time"
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/analyticstypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/zeus"
@@ -25,17 +23,16 @@ type provider struct {
config licensing.Config
settings factory.ScopedProviderSettings
orgGetter organization.Getter
analytics analytics.Analytics
stopChan chan struct{}
}
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) {
return New(ctx, providerSettings, config, store, zeus, orgGetter, analytics)
return New(ctx, providerSettings, config, store, zeus, orgGetter)
})
}
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) (licensing.Licensing, error) {
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) (licensing.Licensing, error) {
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/ee/licensing/httplicensing")
licensestore := sqllicensingstore.New(sqlstore)
return &provider{
@@ -45,7 +42,6 @@ func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Conf
settings: settings,
orgGetter: orgGetter,
stopChan: make(chan struct{}),
analytics: analytics,
}, nil
}
@@ -163,25 +159,6 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
return err
}
stats := licensetypes.NewStatsFromLicense(activeLicense)
provider.analytics.Send(ctx,
analyticstypes.Track{
UserId: "stats_" + organizationID.String(),
Event: "License Updated",
Properties: analyticstypes.NewPropertiesFromMap(stats),
Context: &analyticstypes.Context{
Extra: map[string]interface{}{
analyticstypes.KeyGroupID: organizationID.String(),
},
},
},
analyticstypes.Group{
UserId: "stats_" + organizationID.String(),
GroupId: organizationID.String(),
Traits: analyticstypes.NewTraitsFromMap(stats),
},
)
return nil
}

View File

@@ -11,9 +11,11 @@ RUN apk update && \
COPY ./target/${OS}-${TARGETARCH}/signoz /root/signoz
COPY ./conf/prometheus.yml /root/config/prometheus.yml
COPY ./templates/email /root/templates
COPY frontend/build/ /etc/signoz/web/
RUN chmod 755 /root /root/signoz
ENTRYPOINT ["./signoz"]
ENTRYPOINT ["./signoz"]
CMD ["-config", "/root/config/prometheus.yml"]

View File

@@ -12,9 +12,11 @@ RUN apk update && \
rm -rf /var/cache/apk/*
COPY ./target/${OS}-${ARCH}/signoz /root/signoz
COPY ./conf/prometheus.yml /root/config/prometheus.yml
COPY ./templates/email /root/templates
COPY frontend/build/ /etc/signoz/web/
RUN chmod 755 /root /root/signoz
ENTRYPOINT ["./signoz"]
CMD ["-config", "/root/config/prometheus.yml"]

View File

@@ -7,6 +7,7 @@ import (
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
"github.com/SigNoz/signoz/ee/query-service/interfaces"
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apis/fields"
@@ -16,7 +17,6 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/signoz"
@@ -26,7 +26,8 @@ import (
)
type APIHandlerOptions struct {
DataConnector interfaces.Reader
DataConnector interfaces.DataConnector
PreferSpanMetrics bool
RulesManager *rules.Manager
UsageManager *usage.Manager
IntegrationsController *integrations.Controller
@@ -50,6 +51,7 @@ type APIHandler struct {
func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler, error) {
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
Reader: opts.DataConnector,
PreferSpanMetrics: opts.PreferSpanMetrics,
RuleManager: opts.RulesManager,
IntegrationsController: opts.IntegrationsController,
CloudIntegrationsController: opts.CloudIntegrationsController,
@@ -59,7 +61,7 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
Signoz: signoz,
QuerierAPI: querierAPI.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.Querier),
QuerierAPI: querierAPI.NewAPI(signoz.Querier),
})
if err != nil {

View File

@@ -96,7 +96,7 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
return
}
nextPage, err := ah.Signoz.Modules.User.PrepareSsoRedirect(ctx, redirectUri, email)
nextPage, err := ah.Signoz.Modules.User.PrepareSsoRedirect(ctx, redirectUri, email, ah.opts.JWT)
if err != nil {
zap.L().Error("[receiveSAML] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri)

View File

@@ -59,7 +59,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
}
}
if constants.IsPreferSpanMetrics {
if ah.opts.PreferSpanMetrics {
for idx, feature := range featureSet {
if feature.Name == licensetypes.UseSpanMetrics {
featureSet[idx].Active = true

View File

@@ -0,0 +1,39 @@
package db
import (
"time"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/prometheus"
basechr "github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/telemetrystore"
)
type ClickhouseReader struct {
conn clickhouse.Conn
appdb sqlstore.SQLStore
*basechr.ClickHouseReader
}
func NewDataConnector(
sqlDB sqlstore.SQLStore,
telemetryStore telemetrystore.TelemetryStore,
prometheus prometheus.Prometheus,
cluster string,
fluxIntervalForTraceDetail time.Duration,
cache cache.Cache,
) *ClickhouseReader {
chReader := basechr.NewReader(sqlDB, telemetryStore, prometheus, cluster, fluxIntervalForTraceDetail, cache)
return &ClickhouseReader{
conn: telemetryStore.ClickhouseDB(),
appdb: sqlDB,
ClickHouseReader: chReader,
}
}
func (r *ClickhouseReader) GetSQLStore() sqlstore.SQLStore {
return r.appdb
}

View File

@@ -6,10 +6,14 @@ import (
"net"
"net/http"
_ "net/http/pprof" // http profiler
"time"
"github.com/gorilla/handlers"
"github.com/jmoiron/sqlx"
"github.com/SigNoz/signoz/ee/query-service/app/api"
"github.com/SigNoz/signoz/ee/query-service/app/db"
"github.com/SigNoz/signoz/ee/query-service/constants"
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
"github.com/SigNoz/signoz/ee/query-service/rules"
"github.com/SigNoz/signoz/ee/query-service/usage"
@@ -28,7 +32,6 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
@@ -38,6 +41,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/healthcheck"
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
"github.com/SigNoz/signoz/pkg/query-service/utils"
"go.uber.org/zap"
)
@@ -55,55 +59,62 @@ type ServerOptions struct {
Jwt *authtypes.JWT
}
// Server runs HTTP, Mux and a grpc server
// Server runs HTTP api service
type Server struct {
config signoz.Config
signoz *signoz.SigNoz
jwt *authtypes.JWT
ruleManager *baserules.Manager
serverOptions *ServerOptions
ruleManager *baserules.Manager
// public http router
httpConn net.Listener
httpServer *http.Server
httpHostPort string
httpConn net.Listener
httpServer *http.Server
// private http
privateConn net.Listener
privateHTTP *http.Server
privateHostPort string
opampServer *opamp.Server
privateConn net.Listener
privateHTTP *http.Server
// Usage manager
usageManager *usage.Manager
opampServer *opamp.Server
unavailableChannel chan healthcheck.Status
}
// HealthCheckStatus returns health check status channel a client can subscribe to
func (s Server) HealthCheckStatus() chan healthcheck.Status {
return s.unavailableChannel
}
// NewServer creates and initializes Server
func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT) (*Server, error) {
gatewayProxy, err := gateway.NewProxy(config.Gateway.URL.String(), gateway.RoutePrefix)
func NewServer(serverOptions *ServerOptions) (*Server, error) {
gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
if err != nil {
return nil, err
}
reader := clickhouseReader.NewReader(
signoz.SQLStore,
signoz.TelemetryStore,
signoz.Prometheus,
signoz.TelemetryStore.Cluster(),
config.Querier.FluxInterval,
signoz.Cache,
fluxIntervalForTraceDetail, err := time.ParseDuration(serverOptions.FluxIntervalForTraceDetail)
if err != nil {
return nil, err
}
reader := db.NewDataConnector(
serverOptions.SigNoz.SQLStore,
serverOptions.SigNoz.TelemetryStore,
serverOptions.SigNoz.Prometheus,
serverOptions.Cluster,
fluxIntervalForTraceDetail,
serverOptions.SigNoz.Cache,
)
rm, err := makeRulesManager(
serverOptions.SigNoz.SQLStore.SQLxDB(),
reader,
signoz.Cache,
signoz.Alertmanager,
signoz.SQLStore,
signoz.TelemetryStore,
signoz.Prometheus,
signoz.Modules.OrgGetter,
serverOptions.SigNoz.Cache,
serverOptions.SigNoz.Alertmanager,
serverOptions.SigNoz.SQLStore,
serverOptions.SigNoz.TelemetryStore,
serverOptions.SigNoz.Prometheus,
serverOptions.SigNoz.Modules.OrgGetter,
)
if err != nil {
@@ -111,16 +122,19 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT)
}
// initiate opamp
opAmpModel.Init(signoz.SQLStore, signoz.Instrumentation.Logger(), signoz.Modules.OrgGetter)
_, err = opAmpModel.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB())
if err != nil {
return nil, err
}
integrationsController, err := integrations.NewController(signoz.SQLStore)
integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore)
if err != nil {
return nil, fmt.Errorf(
"couldn't create integrations controller: %w", err,
)
}
cloudIntegrationsController, err := cloudintegrations.NewController(signoz.SQLStore)
cloudIntegrationsController, err := cloudintegrations.NewController(serverOptions.SigNoz.SQLStore)
if err != nil {
return nil, fmt.Errorf(
"couldn't create cloud provider integrations controller: %w", err,
@@ -129,8 +143,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT)
// ingestion pipelines manager
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
signoz.SQLStore,
integrationsController.GetPipelinesForInstalledIntegrations,
serverOptions.SigNoz.SQLStore, integrationsController.GetPipelinesForInstalledIntegrations,
)
if err != nil {
return nil, err
@@ -138,7 +151,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT)
// initiate agent config handler
agentConfMgr, err := agentConf.Initiate(&agentConf.ManagerOptions{
Store: signoz.SQLStore,
DB: serverOptions.SigNoz.SQLStore.SQLxDB(),
AgentFeatures: []agentConf.AgentFeature{logParsingPipelineController},
})
if err != nil {
@@ -146,7 +159,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT)
}
// start the usagemanager
usageManager, err := usage.New(signoz.Licensing, signoz.TelemetryStore.ClickhouseDB(), signoz.Zeus, signoz.Modules.OrgGetter)
usageManager, err := usage.New(serverOptions.SigNoz.Licensing, serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus, serverOptions.SigNoz.Modules.OrgGetter)
if err != nil {
return nil, err
}
@@ -155,36 +168,47 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT)
return nil, err
}
telemetry.GetInstance().SetReader(reader)
telemetry.GetInstance().SetSqlStore(serverOptions.SigNoz.SQLStore)
telemetry.GetInstance().SetSaasOperator(constants.SaasSegmentKey)
telemetry.GetInstance().SetSavedViewsInfoCallback(telemetry.GetSavedViewsInfo)
telemetry.GetInstance().SetAlertsInfoCallback(telemetry.GetAlertsInfo)
telemetry.GetInstance().SetGetUsersCallback(telemetry.GetUsers)
telemetry.GetInstance().SetUserCountCallback(telemetry.GetUserCount)
telemetry.GetInstance().SetDashboardsInfoCallback(telemetry.GetDashboardsInfo)
fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval)
if err != nil {
return nil, err
}
apiOpts := api.APIHandlerOptions{
DataConnector: reader,
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
RulesManager: rm,
UsageManager: usageManager,
IntegrationsController: integrationsController,
CloudIntegrationsController: cloudIntegrationsController,
LogsParsingPipelineController: logParsingPipelineController,
FluxInterval: config.Querier.FluxInterval,
FluxInterval: fluxInterval,
Gateway: gatewayProxy,
GatewayUrl: config.Gateway.URL.String(),
JWT: jwt,
GatewayUrl: serverOptions.GatewayUrl,
JWT: serverOptions.Jwt,
}
apiHandler, err := api.NewAPIHandler(apiOpts, signoz)
apiHandler, err := api.NewAPIHandler(apiOpts, serverOptions.SigNoz)
if err != nil {
return nil, err
}
s := &Server{
config: config,
signoz: signoz,
jwt: jwt,
ruleManager: rm,
httpHostPort: baseconst.HTTPHostPort,
privateHostPort: baseconst.PrivateHostPort,
serverOptions: serverOptions,
unavailableChannel: make(chan healthcheck.Status),
usageManager: usageManager,
}
httpServer, err := s.createPublicServer(apiHandler, signoz.Web)
httpServer, err := s.createPublicServer(apiHandler, serverOptions.SigNoz.Web)
if err != nil {
return nil, err
@@ -200,7 +224,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT)
s.privateHTTP = privateServer
s.opampServer = opamp.InitializeServer(
&opAmpModel.AllAgents, agentConfMgr, signoz.Instrumentation,
&opAmpModel.AllAgents, agentConfMgr,
)
orgs, err := apiHandler.Signoz.Modules.OrgGetter.ListByOwnedKeyRange(context.Background())
@@ -217,22 +241,18 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT)
return s, nil
}
// HealthCheckStatus returns health check status channel a client can subscribe to
func (s Server) HealthCheckStatus() chan healthcheck.Status {
return s.unavailableChannel
}
func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server, error) {
r := baseapp.NewRouter()
r.Use(middleware.NewAuth(s.jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.signoz.Sharder, s.signoz.Instrumentation.Logger()).Wrap)
r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap)
r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(),
s.config.APIServer.Timeout.ExcludedRoutes,
s.config.APIServer.Timeout.Default,
s.config.APIServer.Timeout.Max,
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.serverOptions.SigNoz.Sharder, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.SigNoz.Sharder).Wrap)
r.Use(middleware.NewTimeout(s.serverOptions.SigNoz.Instrumentation.Logger(),
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
s.serverOptions.Config.APIServer.Timeout.Default,
s.serverOptions.Config.APIServer.Timeout.Max,
).Wrap)
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
r.Use(middleware.NewAnalytics().Wrap)
r.Use(middleware.NewLogging(s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.Config.APIServer.Logging.ExcludedRoutes).Wrap)
apiHandler.RegisterPrivateRoutes(r)
@@ -254,16 +274,17 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*http.Server, error) {
r := baseapp.NewRouter()
am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger())
am := middleware.NewAuthZ(s.serverOptions.SigNoz.Instrumentation.Logger())
r.Use(middleware.NewAuth(s.jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.signoz.Sharder, s.signoz.Instrumentation.Logger()).Wrap)
r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap)
r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(),
s.config.APIServer.Timeout.ExcludedRoutes,
s.config.APIServer.Timeout.Default,
s.config.APIServer.Timeout.Max,
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.serverOptions.SigNoz.Sharder, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.SigNoz.Sharder).Wrap)
r.Use(middleware.NewTimeout(s.serverOptions.SigNoz.Instrumentation.Logger(),
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
s.serverOptions.Config.APIServer.Timeout.Default,
s.serverOptions.Config.APIServer.Timeout.Max,
).Wrap)
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
r.Use(middleware.NewAnalytics().Wrap)
r.Use(middleware.NewLogging(s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.Config.APIServer.Logging.ExcludedRoutes).Wrap)
apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am)
@@ -304,7 +325,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
func (s *Server) initListeners() error {
// listen on public port
var err error
publicHostPort := s.httpHostPort
publicHostPort := s.serverOptions.HTTPHostPort
if publicHostPort == "" {
return fmt.Errorf("baseconst.HTTPHostPort is required")
}
@@ -314,10 +335,10 @@ func (s *Server) initListeners() error {
return err
}
zap.L().Info(fmt.Sprintf("Query server started listening on %s...", s.httpHostPort))
zap.L().Info(fmt.Sprintf("Query server started listening on %s...", s.serverOptions.HTTPHostPort))
// listen on private port to support internal services
privateHostPort := s.privateHostPort
privateHostPort := s.serverOptions.PrivateHostPort
if privateHostPort == "" {
return fmt.Errorf("baseconst.PrivateHostPort is required")
@@ -327,7 +348,7 @@ func (s *Server) initListeners() error {
if err != nil {
return err
}
zap.L().Info(fmt.Sprintf("Query server started listening on private port %s...", s.privateHostPort))
zap.L().Info(fmt.Sprintf("Query server started listening on private port %s...", s.serverOptions.PrivateHostPort))
return nil
}
@@ -347,7 +368,7 @@ func (s *Server) Start(ctx context.Context) error {
}
go func() {
zap.L().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.httpHostPort))
zap.L().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.serverOptions.HTTPHostPort))
switch err := s.httpServer.Serve(s.httpConn); err {
case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
@@ -373,7 +394,7 @@ func (s *Server) Start(ctx context.Context) error {
}
go func() {
zap.L().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.privateHostPort))
zap.L().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.serverOptions.PrivateHostPort))
switch err := s.privateHTTP.Serve(s.privateConn); err {
case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
@@ -425,6 +446,7 @@ func (s *Server) Stop(ctx context.Context) error {
}
func makeRulesManager(
db *sqlx.DB,
ch baseint.Reader,
cache cache.Cache,
alertmanager alertmanager.Alertmanager,
@@ -437,6 +459,7 @@ func makeRulesManager(
managerOpts := &baserules.ManagerOptions{
TelemetryStore: telemetryStore,
Prometheus: prometheus,
DBConn: db,
Context: context.Background(),
Logger: zap.L(),
Reader: ch,

View File

@@ -37,14 +37,9 @@ func GetDefaultSiteURL() string {
const DotMetricsEnabled = "DOT_METRICS_ENABLED"
var IsDotMetricsEnabled = false
var IsPreferSpanMetrics = false
func init() {
if GetOrDefaultEnv(DotMetricsEnabled, "false") == "true" {
IsDotMetricsEnabled = true
}
if GetOrDefaultEnv("USE_SPAN_METRICS", "false") == "true" {
IsPreferSpanMetrics = true
}
}

View File

@@ -0,0 +1,11 @@
package interfaces
import (
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
)
// Connector defines methods for interaction
// with o11y data. for example - clickhouse
type DataConnector interface {
baseint.Reader
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
"github.com/SigNoz/signoz/ee/zeus"
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/config"
"github.com/SigNoz/signoz/pkg/config/envprovider"
"github.com/SigNoz/signoz/pkg/config/fileprovider"
@@ -102,14 +101,10 @@ func main() {
fileprovider.NewFactory(),
},
}, signoz.DeprecatedFlags{
MaxIdleConns: maxIdleConns,
MaxOpenConns: maxOpenConns,
DialTimeout: dialTimeout,
Config: promConfigPath,
FluxInterval: fluxInterval,
FluxIntervalForTraceDetail: fluxIntervalForTraceDetail,
Cluster: cluster,
GatewayUrl: gatewayUrl,
MaxIdleConns: maxIdleConns,
MaxOpenConns: maxOpenConns,
DialTimeout: dialTimeout,
Config: promConfigPath,
})
if err != nil {
zap.L().Fatal("Failed to create config", zap.Error(err))
@@ -139,8 +134,8 @@ func main() {
zeus.Config(),
httpzeus.NewProviderFactory(),
licensing.Config(24*time.Hour, 3),
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter, analytics)
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter)
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
@@ -152,7 +147,20 @@ func main() {
zap.L().Fatal("Failed to create signoz", zap.Error(err))
}
server, err := app.NewServer(config, signoz, jwt)
serverOptions := &app.ServerOptions{
Config: config,
SigNoz: signoz,
HTTPHostPort: baseconst.HTTPHostPort,
PreferSpanMetrics: preferSpanMetrics,
PrivateHostPort: baseconst.PrivateHostPort,
FluxInterval: fluxInterval,
FluxIntervalForTraceDetail: fluxIntervalForTraceDetail,
Cluster: cluster,
GatewayUrl: gatewayUrl,
Jwt: jwt,
}
server, err := app.NewServer(serverOptions)
if err != nil {
zap.L().Fatal("Failed to create server", zap.Error(err))
}

View File

@@ -17,21 +17,19 @@ var (
)
var (
Org = "org"
User = "user"
UserNoCascade = "user_no_cascade"
FactorPassword = "factor_password"
CloudIntegration = "cloud_integration"
AgentConfigVersion = "agent_config_version"
Org = "org"
User = "user"
UserNoCascade = "user_no_cascade"
FactorPassword = "factor_password"
CloudIntegration = "cloud_integration"
)
var (
OrgReference = `("org_id") REFERENCES "organizations" ("id")`
UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
UserReferenceNoCascade = `("user_id") REFERENCES "users" ("id")`
FactorPasswordReference = `("password_id") REFERENCES "factor_password" ("id")`
CloudIntegrationReference = `("cloud_integration_id") REFERENCES "cloud_integration" ("id") ON DELETE CASCADE`
AgentConfigVersionReference = `("version_id") REFERENCES "agent_config_version" ("id")`
OrgReference = `("org_id") REFERENCES "organizations" ("id")`
UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
UserReferenceNoCascade = `("user_id") REFERENCES "users" ("id")`
FactorPasswordReference = `("password_id") REFERENCES "factor_password" ("id")`
CloudIntegrationReference = `("cloud_integration_id") REFERENCES "cloud_integration" ("id") ON DELETE CASCADE`
)
type dialect struct{}
@@ -276,8 +274,6 @@ func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.I
fkReferences = append(fkReferences, FactorPasswordReference)
} else if reference == CloudIntegration && !slices.Contains(fkReferences, CloudIntegrationReference) {
fkReferences = append(fkReferences, CloudIntegrationReference)
} else if reference == AgentConfigVersion && !slices.Contains(fkReferences, AgentConfigVersionReference) {
fkReferences = append(fkReferences, AgentConfigVersionReference)
}
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5/stdlib"
"github.com/jmoiron/sqlx"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
)
@@ -18,6 +19,7 @@ type provider struct {
settings factory.ScopedProviderSettings
sqldb *sql.DB
bundb *sqlstore.BunDB
sqlxdb *sqlx.DB
dialect *dialect
}
@@ -59,6 +61,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
settings: settings,
sqldb: sqldb,
bundb: sqlstore.NewBunDB(settings, sqldb, pgdialect.New(), hooks),
sqlxdb: sqlx.NewDb(sqldb, "postgres"),
dialect: new(dialect),
}, nil
}
@@ -71,6 +74,10 @@ func (provider *provider) SQLDB() *sql.DB {
return provider.sqldb
}
func (provider *provider) SQLxDB() *sqlx.DB {
return provider.sqlxdb
}
func (provider *provider) Dialect() sqlstore.SQLDialect {
return provider.dialect
}

View File

@@ -1,5 +1,4 @@
module.exports = {
ignorePatterns: ['src/parser/*.ts'],
env: {
browser: true,
es2021: true,

View File

@@ -1,154 +0,0 @@
# QuerySearch Component Documentation
## Overview
The QuerySearch component is a sophisticated query builder interface that allows users to construct complex search queries with real-time validation and autocomplete functionality.
## Dependencies
```typescript
// Core UI
import { Card, Collapse, Space, Tag, Typography } from 'antd';
// Code Editor
import {
autocompletion,
CompletionContext,
CompletionResult,
startCompletion,
} from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import { ViewPlugin, ViewUpdate } from '@codemirror/view';
import { copilot } from '@uiw/codemirror-theme-copilot';
import CodeMirror, { EditorView, Extension } from '@uiw/react-codemirror';
// Custom Hooks and Utilities
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
import { queryOperatorSuggestions, validateQuery } from 'utils/antlrQueryUtils';
import { getQueryContextAtCursor } from 'utils/queryContextUtils';
```
## Key Features
1. Real-time query validation
2. Context-aware autocompletion
3. Support for various query operators (=, !=, IN, LIKE, etc.)
4. Support for complex conditions with AND/OR operators
5. Support for functions (HAS, HASANY, HASALL)
6. Support for parentheses and nested conditions
7. Query examples for common use cases
## State Management
```typescript
const [query, setQuery] = useState<string>('');
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
const [activeKey, setActiveKey] = useState<string>('');
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
const [queryContext, setQueryContext] = useState<IQueryContext | null>(null);
const [validation, setValidation] = useState<IValidationResult>({...});
const [editingMode, setEditingMode] = useState<'key' | 'operator' | 'value' | 'conjunction' | 'function' | 'parenthesis' | 'bracketList' | null>(null);
```
## Core Functions
### 1. Autocomplete Handler
```typescript
function myCompletions(context: CompletionContext): CompletionResult | null {
// Handles autocomplete suggestions based on context
// Supports different contexts: key, operator, value, function, etc.
}
```
### 2. Value Suggestions Fetcher
```typescript
const fetchValueSuggestions = useCallback(
async (key: string): Promise<void> => {
// Fetches value suggestions for a given key
// Handles loading states and error cases
},
[activeKey, isLoadingSuggestions],
);
```
### 3. Query Change Handler
```typescript
const handleQueryChange = useCallback(async (newQuery: string) => {
// Updates query and validates it
// Handles validation errors
}, []);
```
## Query Context Types
1. Key context: When editing a field name
2. Operator context: When selecting an operator
3. Value context: When entering a value
4. Conjunction context: When using AND/OR
5. Function context: When using functions
6. Parenthesis context: When using parentheses
7. Bracket list context: When using IN operator
## Example Queries
```typescript
const queryExamples = [
{ label: 'Basic Query', query: "status = 'error'" },
{ label: 'Multiple Conditions', query: "status = 'error' AND service = 'frontend'" },
{ label: 'IN Operator', query: "status IN ['error', 'warning']" },
{ label: 'Function Usage', query: "HAS(service, 'frontend')" },
{ label: 'Numeric Comparison', query: 'duration > 1000' },
// ... more examples
];
```
## Performance Optimizations
1. Uses `useCallback` for memoized functions
2. Tracks component mount state to prevent updates after unmount
3. Debounces suggestion fetching
4. Caches key suggestions
## Error Handling
```typescript
try {
const validationResponse = validateQuery(newQuery);
setValidation(validationResponse);
} catch (error) {
setValidation({
isValid: false,
message: 'Failed to process query',
errors: [error as IDetailedError],
});
}
```
## Usage Example
```typescript
<QuerySearch />
```
## Styling
- Uses SCSS for styling
- Custom classes for different components
- Theme integration with CodeMirror
## Best Practices
1. Always validate queries before submission
2. Handle loading states appropriately
3. Provide clear error messages
4. Use appropriate operators for different data types
5. Consider performance implications of complex queries
## Common Issues and Solutions
1. Query validation errors
- Check syntax and operator usage
- Verify data types match operator requirements
2. Performance issues
- Optimize suggestion fetching
- Cache frequently used values
3. UI/UX issues
- Ensure clear error messages
- Provide helpful suggestions
- Show appropriate loading states
## Future Improvements
1. Add more query examples
2. Enhance error messages
3. Improve performance for large datasets
4. Add more operator support
5. Enhance UI/UX features

View File

@@ -28,8 +28,6 @@
"dependencies": {
"@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.8.0",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/lang-javascript": "6.2.3",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
@@ -45,8 +43,6 @@
"@signozhq/design-tokens": "1.1.4",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/react-codemirror": "4.23.10",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/hierarchy": "3.12.0",
@@ -57,7 +53,6 @@
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"axios": "1.8.2",
"antlr4": "4.13.2",
"babel-eslint": "^10.1.0",
"babel-jest": "^29.6.4",
"babel-loader": "9.1.3",
@@ -83,7 +78,7 @@
"fontfaceobserver": "2.3.0",
"history": "4.10.1",
"html-webpack-plugin": "5.5.0",
"http-proxy-middleware": "3.0.5",
"http-proxy-middleware": "3.0.3",
"http-status-codes": "2.3.0",
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
@@ -255,7 +250,7 @@
"xml2js": "0.5.0",
"phin": "^3.7.1",
"body-parser": "1.20.3",
"http-proxy-middleware": "3.0.5",
"http-proxy-middleware": "3.0.3",
"cross-spawn": "7.0.5",
"cookie": "^0.7.1",
"serialize-javascript": "6.0.2",

View File

@@ -9,8 +9,8 @@
"tooltip_notification_channels": "More details on how to setting notification channels",
"sending_channels_note": "The alerts will be sent to all the configured channels.",
"loading_channels_message": "Loading Channels..",
"page_title_create": "New Notification Channel",
"page_title_edit": "Edit Notification Channel",
"page_title_create": "New Notification Channels",
"page_title_edit": "Edit Notification Channels",
"button_save_channel": "Save",
"button_test_channel": "Test",
"button_return": "Back",
@@ -62,8 +62,5 @@
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
"webhook_url_required": "Webhook URL is mandatory",
"slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)",
"api_key_required": "API Key is mandatory",
"to_required": "To field is mandatory",
"channel_name_required": "Channel name is mandatory"
"slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)"
}

View File

@@ -129,6 +129,5 @@
"text_num_points": "data points in each result group",
"text_alert_frequency": "Run alert every",
"text_for": "minutes",
"selected_query_placeholder": "Select query",
"alert_rule_not_found": "Alert Rule not found"
"selected_query_placeholder": "Select query"
}

View File

@@ -9,8 +9,8 @@
"tooltip_notification_channels": "More details on how to setting notification channels",
"sending_channels_note": "The alerts will be sent to all the configured channels.",
"loading_channels_message": "Loading Channels..",
"page_title_create": "New Notification Channel",
"page_title_edit": "Edit Notification Channel",
"page_title_create": "New Notification Channels",
"page_title_edit": "Edit Notification Channels",
"button_save_channel": "Save",
"button_test_channel": "Test",
"button_return": "Back",
@@ -77,8 +77,5 @@
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
"webhook_url_required": "Webhook URL is mandatory",
"slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)",
"api_key_required": "API Key is mandatory",
"to_required": "To field is mandatory",
"channel_name_required": "Channel name is mandatory"
"slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)"
}

View File

@@ -3,7 +3,6 @@ import setLocalStorageApi from 'api/browser/localstorage/set';
import getAll from 'api/v1/user/get';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
@@ -15,7 +14,6 @@ import { matchPath, useLocation } from 'react-router-dom';
import { SuccessResponseV2 } from 'types/api';
import APIError from 'types/api/error';
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
import { OrgPreference } from 'types/api/preferences/preference';
import { Organization } from 'types/api/user/getOrganization';
import { UserResponse } from 'types/api/user/getUser';
import { USER_ROLES } from 'types/roles';
@@ -97,8 +95,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
usersData.data
) {
const isOnboardingComplete = orgPreferences?.find(
(preference: OrgPreference) =>
preference.name === ORG_PREFERENCES.ORG_ONBOARDING,
(preference: Record<string, any>) => preference.name === 'org_onboarding',
)?.value;
const isFirstUser = checkFirstTimeUser();
@@ -126,8 +123,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const isRouteEnabledForWorkspaceBlockedState =
isAdmin &&
(path === ROUTES.SETTINGS ||
path === ROUTES.ORG_SETTINGS ||
(path === ROUTES.ORG_SETTINGS ||
path === ROUTES.BILLING ||
path === ROUTES.MY_SETTINGS);

View File

@@ -71,7 +71,7 @@ function App(): JSX.Element {
const orgName =
org && Array.isArray(org) && org.length > 0 ? org[0].displayName : '';
const { displayName, email, role, id, orgId } = user;
const { displayName, email, role } = user;
const domain = extractDomain(email);
const hostNameParts = hostname.split('.');
@@ -105,7 +105,7 @@ function App(): JSX.Element {
logEvent('Domain Identified', groupTraits, 'group');
}
if (window && window.Appcues) {
window.Appcues.identify(id, {
window.Appcues.identify(email, {
name: displayName,
tenant_id: hostNameParts[0],
@@ -131,7 +131,7 @@ function App(): JSX.Element {
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
});
posthog?.identify(id, {
posthog?.identify(email, {
email,
name: displayName,
orgName,
@@ -143,7 +143,7 @@ function App(): JSX.Element {
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
});
posthog?.group('company', orgId, {
posthog?.group('company', domain, {
name: orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
@@ -193,12 +193,11 @@ function App(): JSX.Element {
updatedRoutes = updatedRoutes.filter(
(route) => route?.path !== ROUTES.BILLING,
);
}
if (isEnterpriseSelfHostedUser) {
updatedRoutes.push(LIST_LICENSES);
if (isEnterpriseSelfHostedUser) {
updatedRoutes.push(LIST_LICENSES);
}
}
// always add support route for cloud users
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];
} else {

View File

@@ -128,7 +128,12 @@ export const AlertOverview = Loadable(
);
export const CreateAlertChannelAlerts = Loadable(
() => import(/* webpackChunkName: "Create Channels" */ 'pages/Settings'),
() =>
import(/* webpackChunkName: "Create Channels" */ 'pages/AlertChannelCreate'),
);
export const EditAlertChannelsAlerts = Loadable(
() => import(/* webpackChunkName: "Edit Channels" */ 'pages/ChannelsEdit'),
);
export const AllAlertChannels = Loadable(
@@ -160,7 +165,7 @@ export const APIKeys = Loadable(
);
export const MySettings = Loadable(
() => import(/* webpackChunkName: "All MySettings" */ 'pages/Settings'),
() => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'),
);
export const CustomDomainSettings = Loadable(
@@ -217,7 +222,7 @@ export const LogsIndexToFields = Loadable(
);
export const BillingPage = Loadable(
() => import(/* webpackChunkName: "BillingPage" */ 'pages/Settings'),
() => import(/* webpackChunkName: "BillingPage" */ 'pages/Billing'),
);
export const SupportPage = Loadable(
@@ -244,7 +249,7 @@ export const WorkspaceAccessRestricted = Loadable(
);
export const ShortcutsPage = Loadable(
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Settings'),
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
);
export const InstalledIntegrations = Loadable(

View File

@@ -7,15 +7,20 @@ import {
AlertOverview,
AllAlertChannels,
AllErrors,
APIKeys,
ApiMonitoring,
BillingPage,
CreateAlertChannelAlerts,
CreateNewAlerts,
CustomDomainSettings,
DashboardPage,
DashboardWidget,
EditAlertChannelsAlerts,
EditRulesPage,
ErrorDetails,
Home,
InfrastructureMonitoring,
IngestionSettings,
InstalledIntegrations,
LicensePage,
ListAllALertsPage,
@@ -26,10 +31,12 @@ import {
LogsIndexToFields,
LogsSaveViews,
MetricsExplorer,
MySettings,
NewDashboardPage,
OldLogsExplorer,
Onboarding,
OnboardingV2,
OrganizationSettings,
OrgOnboarding,
PasswordReset,
PipelinePage,
@@ -38,6 +45,7 @@ import {
ServicesTablePage,
ServiceTopLevelOperationsPage,
SettingsPage,
ShortcutsPage,
SignupPage,
SomethingWentWrong,
StatusPage,
@@ -142,7 +150,7 @@ const routes: AppRoutes[] = [
},
{
path: ROUTES.SETTINGS,
exact: false,
exact: true,
component: SettingsPage,
isPrivate: true,
key: 'SETTINGS',
@@ -252,6 +260,13 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'CHANNELS_NEW',
},
{
path: ROUTES.CHANNELS_EDIT,
exact: true,
component: EditAlertChannelsAlerts,
isPrivate: true,
key: 'CHANNELS_EDIT',
},
{
path: ROUTES.ALL_CHANNELS,
exact: true,
@@ -280,6 +295,41 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'VERSION',
},
{
path: ROUTES.ORG_SETTINGS,
exact: true,
component: OrganizationSettings,
isPrivate: true,
key: 'ORG_SETTINGS',
},
{
path: ROUTES.INGESTION_SETTINGS,
exact: true,
component: IngestionSettings,
isPrivate: true,
key: 'INGESTION_SETTINGS',
},
{
path: ROUTES.API_KEYS,
exact: true,
component: APIKeys,
isPrivate: true,
key: 'API_KEYS',
},
{
path: ROUTES.MY_SETTINGS,
exact: true,
component: MySettings,
isPrivate: true,
key: 'MY_SETTINGS',
},
{
path: ROUTES.CUSTOM_DOMAIN_SETTINGS,
exact: true,
component: CustomDomainSettings,
isPrivate: true,
key: 'CUSTOM_DOMAIN_SETTINGS',
},
{
path: ROUTES.LOGS,
exact: true,
@@ -343,6 +393,13 @@ const routes: AppRoutes[] = [
key: 'SOMETHING_WENT_WRONG',
isPrivate: false,
},
{
path: ROUTES.BILLING,
exact: true,
component: BillingPage,
key: 'BILLING',
isPrivate: true,
},
{
path: ROUTES.WORKSPACE_LOCKED,
exact: true,
@@ -364,6 +421,13 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'WORKSPACE_ACCESS_RESTRICTED',
},
{
path: ROUTES.SHORTCUTS,
exact: true,
component: ShortcutsPage,
isPrivate: true,
key: 'SHORTCUTS',
},
{
path: ROUTES.INTEGRATIONS,
exact: true,

View File

@@ -3,7 +3,6 @@ const apiV1 = '/api/v1/';
export const apiV2 = '/api/v2/';
export const apiV3 = '/api/v3/';
export const apiV4 = '/api/v4/';
export const apiV5 = '/api/v5/';
export const gatewayApiV1 = '/api/gateway/v1/';
export const gatewayApiV2 = '/api/gateway/v2/';
export const apiAlertManager = '/api/alertmanager/';

View File

@@ -1,29 +0,0 @@
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import axios, { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { ChangelogSchema } from 'types/api/changelog/getChangelogByVersion';
const getChangelogByVersion = async (
versionId: string,
): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> => {
try {
const response = await axios.get(`
https://cms.signoz.cloud/api/release-changelogs?filters[version][$eq]=${versionId}&populate[features][sort]=sort_order:asc&populate[features][populate][media][fields]=id,ext,url,mime,alternativeText
`);
if (!Array.isArray(response.data.data) || response.data.data.length === 0) {
throw new Error('No changelog found!');
}
return {
statusCode: 200,
error: null,
message: response.statusText,
payload: response.data.data[0],
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getChangelogByVersion;

View File

@@ -19,7 +19,6 @@ import apiV1, {
apiV2,
apiV3,
apiV4,
apiV5,
gatewayApiV1,
gatewayApiV2,
} from './apiV1';
@@ -172,18 +171,6 @@ ApiV4Instance.interceptors.response.use(
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
//
// axios V5
export const ApiV5Instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV5}`,
});
ApiV5Instance.interceptors.response.use(
interceptorsResponse,
interceptorRejected,
);
ApiV5Instance.interceptors.request.use(interceptorsRequestResponse);
//
// axios Base
export const ApiBaseInstance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,

View File

@@ -1,17 +0,0 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import {
QueryKeyRequestProps,
QueryKeySuggestionsResponseProps,
} from 'types/api/querySuggestions/types';
export const getKeySuggestions = (
props: QueryKeyRequestProps,
): Promise<AxiosResponse<QueryKeySuggestionsResponseProps>> =>
axios.get(
`/fields/keys?signal=${props.signal}&searchText=${
props.searchText
}&metricName=${props.metricName ?? ''}&fieldContext=${
props.fieldContext ?? ''
}&fieldDataType=${props.fieldDataType ?? ''}`,
);

View File

@@ -1,11 +0,0 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import {
QueryKeyValueRequestProps,
QueryKeyValueSuggestionsResponseProps,
} from 'types/api/querySuggestions/types';
export const getValueSuggestions = (
props: QueryKeyValueRequestProps,
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> =>
axios.get(`/fields/values?signal=${props.signal}&name=${props.key}`);

View File

@@ -119,7 +119,6 @@ export const updateFunnelSteps = async (
export interface ValidateFunnelPayload {
start_time: number;
end_time: number;
steps: FunnelStepData[];
}
export interface ValidateFunnelResponse {
@@ -133,11 +132,12 @@ export interface ValidateFunnelResponse {
}
export const validateFunnelSteps = async (
funnelId: string,
payload: ValidateFunnelPayload,
signal?: AbortSignal,
): Promise<SuccessResponse<ValidateFunnelResponse> | ErrorResponse> => {
const response = await axios.post(
`${FUNNELS_BASE_PATH}/analytics/validate`,
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/validate`,
payload,
{ signal },
);
@@ -185,7 +185,6 @@ export interface FunnelOverviewPayload {
end_time: number;
step_start?: number;
step_end?: number;
steps: FunnelStepData[];
}
export interface FunnelOverviewResponse {
@@ -203,11 +202,12 @@ export interface FunnelOverviewResponse {
}
export const getFunnelOverview = async (
funnelId: string,
payload: FunnelOverviewPayload,
signal?: AbortSignal,
): Promise<SuccessResponse<FunnelOverviewResponse> | ErrorResponse> => {
const response = await axios.post(
`${FUNNELS_BASE_PATH}/analytics/overview`,
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/overview`,
payload,
{
signal,
@@ -235,11 +235,12 @@ export interface SlowTraceData {
}
export const getFunnelSlowTraces = async (
funnelId: string,
payload: FunnelOverviewPayload,
signal?: AbortSignal,
): Promise<SuccessResponse<SlowTraceData> | ErrorResponse> => {
const response = await axios.post(
`${FUNNELS_BASE_PATH}/analytics/slow-traces`,
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/slow-traces`,
payload,
{
signal,
@@ -272,7 +273,7 @@ export const getFunnelErrorTraces = async (
signal?: AbortSignal,
): Promise<SuccessResponse<ErrorTraceData> | ErrorResponse> => {
const response: AxiosResponse = await axios.post(
`${FUNNELS_BASE_PATH}/analytics/error-traces`,
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/error-traces`,
payload,
{
signal,
@@ -290,7 +291,6 @@ export const getFunnelErrorTraces = async (
export interface FunnelStepsPayload {
start_time: number;
end_time: number;
steps: FunnelStepData[];
}
export interface FunnelStepGraphMetrics {
@@ -307,11 +307,12 @@ export interface FunnelStepsResponse {
}
export const getFunnelSteps = async (
funnelId: string,
payload: FunnelStepsPayload,
signal?: AbortSignal,
): Promise<SuccessResponse<FunnelStepsResponse> | ErrorResponse> => {
const response = await axios.post(
`${FUNNELS_BASE_PATH}/analytics/steps`,
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/steps`,
payload,
{ signal },
);
@@ -329,7 +330,6 @@ export interface FunnelStepsOverviewPayload {
end_time: number;
step_start?: number;
step_end?: number;
steps: FunnelStepData[];
}
export interface FunnelStepsOverviewResponse {
@@ -341,11 +341,12 @@ export interface FunnelStepsOverviewResponse {
}
export const getFunnelStepsOverview = async (
funnelId: string,
payload: FunnelStepsOverviewPayload,
signal?: AbortSignal,
): Promise<SuccessResponse<FunnelStepsOverviewResponse> | ErrorResponse> => {
const response = await axios.post(
`${FUNNELS_BASE_PATH}/analytics/steps/overview`,
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/steps/overview`,
payload,
{ signal },
);

View File

@@ -0,0 +1,21 @@
import axios from 'api';
import { Props } from 'types/api/userFeedback/sendResponse';
const sendFeedback = async (props: Props): Promise<number> => {
const response = await axios.post(
'/feedback',
{
email: props.email,
message: props.message,
},
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
},
);
return response.status;
};
export default sendFeedback;

View File

@@ -1,168 +0,0 @@
// V5 Query Range Constants
import { ENTITY_VERSION_V5 } from 'constants/app';
import {
FunctionName,
RequestType,
SignalType,
Step,
} from 'types/api/v5/queryRange';
// ===================== Schema and Version Constants =====================
export const SCHEMA_VERSION_V5 = ENTITY_VERSION_V5;
export const API_VERSION_V5 = 'v5';
// ===================== Default Values =====================
export const DEFAULT_STEP_INTERVAL: Step = '60s';
export const DEFAULT_LIMIT = 100;
export const DEFAULT_OFFSET = 0;
// ===================== Request Type Constants =====================
export const REQUEST_TYPES: Record<string, RequestType> = {
SCALAR: 'scalar',
TIME_SERIES: 'time_series',
RAW: 'raw',
DISTRIBUTION: 'distribution',
} as const;
// ===================== Signal Type Constants =====================
export const SIGNAL_TYPES: Record<string, SignalType> = {
TRACES: 'traces',
LOGS: 'logs',
METRICS: 'metrics',
} as const;
// ===================== Common Aggregation Expressions =====================
export const TRACE_AGGREGATIONS = {
COUNT: 'count()',
COUNT_DISTINCT_TRACE_ID: 'count_distinct(traceID)',
AVG_DURATION: 'avg(duration_nano)',
P50_DURATION: 'p50(duration_nano)',
P95_DURATION: 'p95(duration_nano)',
P99_DURATION: 'p99(duration_nano)',
MAX_DURATION: 'max(duration_nano)',
MIN_DURATION: 'min(duration_nano)',
SUM_DURATION: 'sum(duration_nano)',
} as const;
export const LOG_AGGREGATIONS = {
COUNT: 'count()',
COUNT_DISTINCT_HOST: 'count_distinct(host.name)',
COUNT_DISTINCT_SERVICE: 'count_distinct(service.name)',
COUNT_DISTINCT_CONTAINER: 'count_distinct(container.name)',
} as const;
// ===================== Common Filter Expressions =====================
export const COMMON_FILTERS = {
// Trace filters
SERVER_SPANS: "kind_string = 'Server'",
CLIENT_SPANS: "kind_string = 'Client'",
INTERNAL_SPANS: "kind_string = 'Internal'",
ERROR_SPANS: 'http.status_code >= 400',
SUCCESS_SPANS: 'http.status_code < 400',
// Common service filters
EXCLUDE_HEALTH_CHECKS: "http.route != '/health' AND http.route != '/ping'",
HTTP_REQUESTS: "http.method != ''",
// Log filters
ERROR_LOGS: "severity_text = 'ERROR'",
WARN_LOGS: "severity_text = 'WARN'",
INFO_LOGS: "severity_text = 'INFO'",
DEBUG_LOGS: "severity_text = 'DEBUG'",
} as const;
// ===================== Common Group By Fields =====================
export const COMMON_GROUP_BY_FIELDS = {
SERVICE_NAME: {
name: 'service.name',
fieldDataType: 'string' as const,
fieldContext: 'resource' as const,
},
HTTP_METHOD: {
name: 'http.method',
fieldDataType: 'string' as const,
fieldContext: 'attribute' as const,
},
HTTP_ROUTE: {
name: 'http.route',
fieldDataType: 'string' as const,
fieldContext: 'attribute' as const,
},
HTTP_STATUS_CODE: {
name: 'http.status_code',
fieldDataType: 'int64' as const,
fieldContext: 'attribute' as const,
},
HOST_NAME: {
name: 'host.name',
fieldDataType: 'string' as const,
fieldContext: 'resource' as const,
},
CONTAINER_NAME: {
name: 'container.name',
fieldDataType: 'string' as const,
fieldContext: 'resource' as const,
},
} as const;
// ===================== Function Names =====================
export const FUNCTION_NAMES: Record<string, FunctionName> = {
CUT_OFF_MIN: 'cutOffMin',
CUT_OFF_MAX: 'cutOffMax',
CLAMP_MIN: 'clampMin',
CLAMP_MAX: 'clampMax',
ABSOLUTE: 'absolute',
RUNNING_DIFF: 'runningDiff',
LOG2: 'log2',
LOG10: 'log10',
CUM_SUM: 'cumSum',
EWMA3: 'ewma3',
EWMA5: 'ewma5',
EWMA7: 'ewma7',
MEDIAN3: 'median3',
MEDIAN5: 'median5',
MEDIAN7: 'median7',
TIME_SHIFT: 'timeShift',
ANOMALY: 'anomaly',
} as const;
// ===================== Common Step Intervals =====================
export const STEP_INTERVALS = {
FIFTEEN_SECONDS: '15s',
THIRTY_SECONDS: '30s',
ONE_MINUTE: '60s',
FIVE_MINUTES: '300s',
TEN_MINUTES: '600s',
FIFTEEN_MINUTES: '900s',
THIRTY_MINUTES: '1800s',
ONE_HOUR: '3600s',
TWO_HOURS: '7200s',
SIX_HOURS: '21600s',
TWELVE_HOURS: '43200s',
ONE_DAY: '86400s',
} as const;
// ===================== Time Range Presets =====================
export const TIME_RANGE_PRESETS = {
LAST_5_MINUTES: 5 * 60 * 1000,
LAST_15_MINUTES: 15 * 60 * 1000,
LAST_30_MINUTES: 30 * 60 * 1000,
LAST_HOUR: 60 * 60 * 1000,
LAST_3_HOURS: 3 * 60 * 60 * 1000,
LAST_6_HOURS: 6 * 60 * 60 * 1000,
LAST_12_HOURS: 12 * 60 * 60 * 1000,
LAST_24_HOURS: 24 * 60 * 60 * 1000,
LAST_3_DAYS: 3 * 24 * 60 * 60 * 1000,
LAST_7_DAYS: 7 * 24 * 60 * 60 * 1000,
} as const;

View File

@@ -1,239 +0,0 @@
import { isEmpty } from 'lodash-es';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
import {
DistributionData,
MetricRangePayloadV5,
RawData,
ScalarData,
TimeSeriesData,
} from 'types/api/v5/queryRange';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
/**
* Converts V5 TimeSeriesData to legacy format
*/
function convertTimeSeriesData(
timeSeriesData: TimeSeriesData,
legendMap: Record<string, string>,
): QueryDataV3 {
// Convert V5 time series format to legacy QueryDataV3 format
return {
queryName: timeSeriesData.queryName,
legend: legendMap[timeSeriesData.queryName] || timeSeriesData.queryName,
series: timeSeriesData?.aggregations?.flatMap((aggregation) =>
aggregation.series.map((series) => ({
labels: series.labels
? Object.fromEntries(
series.labels.map((label) => [label.key.name, label.value]),
)
: {},
labelsArray: series.labels
? series.labels.map((label) => ({ [label.key.name]: label.value }))
: [],
values: series.values.map((value) => ({
timestamp: value.timestamp,
value: String(value.value),
})),
})),
),
list: null,
};
}
/**
* Converts V5 ScalarData array to legacy format with table structure
*/
function convertScalarDataArrayToTable(
scalarDataArray: ScalarData[],
legendMap: Record<string, string>,
): QueryDataV3[] {
// If no scalar data, return empty structure
if (!scalarDataArray || scalarDataArray.length === 0) {
return [];
}
// Process each scalar data separately to maintain query separation
return scalarDataArray?.map((scalarData) => {
// Get query name from the first column
const queryName = scalarData?.columns?.[0]?.queryName || '';
// Collect columns for this specific query
const columns = scalarData?.columns?.map((col) => ({
name: col.columnType === 'aggregation' ? col.queryName : col.name,
queryName: col.queryName,
isValueColumn: col.columnType === 'aggregation',
}));
// Process rows for this specific query
const rows = scalarData?.data?.map((dataRow) => {
const rowData: Record<string, any> = {};
scalarData?.columns?.forEach((col, colIndex) => {
const columnName =
col.columnType === 'aggregation' ? col.queryName : col.name;
rowData[columnName] = dataRow[colIndex];
});
return { data: rowData };
});
return {
queryName,
legend: legendMap[queryName] || '',
series: null,
list: null,
table: {
columns,
rows,
},
};
});
}
/**
* Converts V5 RawData to legacy format
*/
function convertRawData(
rawData: RawData,
legendMap: Record<string, string>,
): QueryDataV3 {
// Convert V5 raw format to legacy QueryDataV3 format
return {
queryName: rawData.queryName,
legend: legendMap[rawData.queryName] || rawData.queryName,
series: null,
list: rawData.rows?.map((row) => ({
timestamp: row.timestamp,
data: {
// Map raw data to ILog structure - spread row.data first to include all properties
...row.data,
date: row.timestamp,
} as any,
})),
};
}
/**
* Converts V5 DistributionData to legacy format
*/
function convertDistributionData(
distributionData: DistributionData,
legendMap: Record<string, string>,
): any {
// eslint-disable-line @typescript-eslint/no-explicit-any
// Convert V5 distribution format to legacy histogram format
return {
...distributionData,
legendMap,
};
}
/**
* Helper function to convert V5 data based on type
*/
function convertV5DataByType(
v5Data: any,
legendMap: Record<string, string>,
): MetricRangePayloadV3['data'] {
switch (v5Data?.type) {
case 'time_series': {
const timeSeriesData = v5Data.data.results as TimeSeriesData[];
return {
resultType: 'time_series',
result: timeSeriesData.map((timeSeries) =>
convertTimeSeriesData(timeSeries, legendMap),
),
};
}
case 'scalar': {
const scalarData = v5Data.data.results as ScalarData[];
// For scalar data, combine all results into separate table entries
const combinedTables = convertScalarDataArrayToTable(scalarData, legendMap);
return {
resultType: 'scalar',
result: combinedTables,
};
}
case 'raw': {
const rawData = v5Data.data.results as RawData[];
return {
resultType: 'raw',
result: rawData.map((raw) => convertRawData(raw, legendMap)),
};
}
case 'distribution': {
const distributionData = v5Data.data.results as DistributionData[];
return {
resultType: 'distribution',
result: distributionData.map((distribution) =>
convertDistributionData(distribution, legendMap),
),
};
}
default:
return {
resultType: '',
result: [],
};
}
}
/**
* Converts V5 API response to legacy format expected by frontend components
*/
export function convertV5ResponseToLegacy(
v5Response: SuccessResponse<MetricRangePayloadV5>,
legendMap: Record<string, string>,
// formatForWeb?: boolean,
): SuccessResponse<MetricRangePayloadV3> {
const { payload } = v5Response;
const v5Data = payload?.data;
// todo - sagar
// If formatForWeb is true, return as-is (like existing logic)
// Exception: scalar data should always be converted to table format
// if (formatForWeb && v5Data?.type !== 'scalar') {
// return v5Response as any;
// }
// Convert based on V5 response type
const convertedData = convertV5DataByType(v5Data, legendMap);
// Create legacy-compatible response structure
const legacyResponse: SuccessResponse<MetricRangePayloadV3> = {
...v5Response,
payload: {
data: convertedData,
},
};
// Apply legend mapping (similar to existing logic)
if (legacyResponse.payload?.data?.result) {
legacyResponse.payload.data.result = legacyResponse.payload.data.result.map(
(queryData: any) => {
// eslint-disable-line @typescript-eslint/no-explicit-any
const newQueryData = queryData;
newQueryData.legend = legendMap[queryData.queryName];
// If metric names is an empty object
if (isEmpty(queryData.metric)) {
// If metrics list is empty && the user haven't defined a legend then add the legend equal to the name of the query.
if (newQueryData.legend === undefined || newQueryData.legend === null) {
newQueryData.legend = queryData.queryName;
}
// If name of the query and the legend if inserted is same then add the same to the metrics object.
if (queryData.queryName === newQueryData.legend) {
newQueryData.metric = newQueryData.metric || {};
newQueryData.metric[queryData.queryName] = queryData.queryName;
}
}
return newQueryData;
},
);
}
return legacyResponse;
}

View File

@@ -1,51 +0,0 @@
import { ApiV5Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MetricRangePayloadV5,
QueryRangePayloadV5,
} from 'types/api/v5/queryRange';
export const getQueryRangeV5 = async (
props: QueryRangePayloadV5,
version: string,
signal: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricRangePayloadV5> | ErrorResponse> => {
try {
if (version && version === ENTITY_VERSION_V5) {
const response = await ApiV5Instance.post('/query_range', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
}
// Default V5 behavior
const response = await ApiV5Instance.post('/query_range', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getQueryRangeV5;

View File

@@ -1,394 +0,0 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEmpty } from 'lodash-es';
import {
IBuilderQuery,
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
FieldContext,
FieldDataType,
FunctionName,
GroupByKey,
LogAggregation,
MetricAggregation,
OrderBy,
QueryEnvelope,
QueryFunction,
QueryRangePayloadV5,
QueryType,
RequestType,
TelemetryFieldKey,
TraceAggregation,
VariableItem,
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
type PrepareQueryRangePayloadV5Result = {
queryPayload: QueryRangePayloadV5;
legendMap: Record<string, string>;
};
/**
* Maps panel types to V5 request types
*/
function mapPanelTypeToRequestType(panelType: PANEL_TYPES): RequestType {
switch (panelType) {
case PANEL_TYPES.TIME_SERIES:
case PANEL_TYPES.BAR:
return 'time_series';
case PANEL_TYPES.TABLE:
case PANEL_TYPES.PIE:
case PANEL_TYPES.VALUE:
case PANEL_TYPES.TRACE:
return 'scalar';
case PANEL_TYPES.LIST:
return 'raw';
case PANEL_TYPES.HISTOGRAM:
return 'distribution';
default:
return '';
}
}
/**
* Gets signal type from data source
*/
function getSignalType(dataSource: string): 'traces' | 'logs' | 'metrics' {
if (dataSource === 'traces') return 'traces';
if (dataSource === 'logs') return 'logs';
return 'metrics';
}
/**
* Creates base spec for builder queries
*/
function createBaseSpec(
queryData: IBuilderQuery,
requestType: RequestType,
panelType?: PANEL_TYPES,
): BaseBuilderQuery {
return {
stepInterval: queryData.stepInterval,
disabled: queryData.disabled,
filter: queryData?.filter?.expression ? queryData.filter : undefined,
groupBy:
queryData.groupBy?.length > 0
? queryData.groupBy.map(
(item: any): GroupByKey => ({
name: item.key,
fieldDataType: item?.dataType,
fieldContext: item?.type,
description: item?.description,
unit: item?.unit,
signal: item?.signal,
materialized: item?.materialized,
}),
)
: undefined,
limit:
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
? queryData.limit || queryData.pageSize || undefined
: queryData.limit || undefined,
offset: requestType === 'raw' ? queryData.offset : undefined,
order:
queryData.orderBy.length > 0
? queryData.orderBy.map(
(order: any): OrderBy => ({
key: {
name: order.columnName,
},
direction: order.order,
}),
)
: undefined,
// legend: isEmpty(queryData.legend) ? undefined : queryData.legend,
having: isEmpty(queryData.havingExpression)
? undefined
: queryData?.havingExpression,
functions: isEmpty(queryData.functions)
? undefined
: queryData.functions.map(
(func: QueryFunctionProps): QueryFunction => ({
name: func.name as FunctionName,
args: func.args.map((arg) => ({
// name: arg.name,
value: arg,
})),
}),
),
selectFields: isEmpty(queryData.selectColumns)
? undefined
: queryData.selectColumns?.map(
(column: any): TelemetryFieldKey => ({
name: column.name ?? column.key,
fieldDataType:
column?.fieldDataType ?? (column?.dataType as FieldDataType),
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
signal: column?.signal ?? undefined,
}),
),
};
}
// Utility to parse aggregation expressions with optional alias
export function parseAggregations(
expression: string,
): { expression: string; alias?: string }[] {
const result: { expression: string; alias?: string }[] = [];
const regex = /([a-zA-Z0-9_]+\([^)]*\))(?:\s*as\s+([a-zA-Z0-9_]+))?/g;
let match = regex.exec(expression);
while (match !== null) {
const expr = match[1];
const alias = match[2];
if (alias) {
result.push({ expression: expr, alias });
} else {
result.push({ expression: expr });
}
match = regex.exec(expression);
}
return result;
}
export function createAggregation(
queryData: any,
): TraceAggregation[] | LogAggregation[] | MetricAggregation[] {
if (queryData.dataSource === DataSource.METRICS) {
return [
{
metricName: queryData?.aggregateAttribute?.key,
temporality: queryData?.aggregateAttribute?.temporality,
timeAggregation: queryData?.timeAggregation,
spaceAggregation: queryData?.spaceAggregation,
},
];
}
if (queryData.aggregations?.length > 0) {
return isEmpty(parseAggregations(queryData.aggregations?.[0].expression))
? [{ expression: 'count()' }]
: parseAggregations(queryData.aggregations?.[0].expression);
}
return [{ expression: 'count()' }];
}
/**
* Converts query builder data to V5 builder queries
*/
function convertBuilderQueriesToV5(
builderQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
requestType: RequestType,
panelType?: PANEL_TYPES,
): QueryEnvelope[] {
return Object.entries(builderQueries).map(
([queryName, queryData]): QueryEnvelope => {
const signal = getSignalType(queryData.dataSource);
const baseSpec = createBaseSpec(queryData, requestType, panelType);
let spec: QueryEnvelope['spec'];
const aggregations = createAggregation(queryData);
switch (signal) {
case 'traces':
spec = {
name: queryName,
signal: 'traces' as const,
...baseSpec,
aggregations: aggregations as TraceAggregation[],
};
break;
case 'logs':
spec = {
name: queryName,
signal: 'logs' as const,
...baseSpec,
aggregations: aggregations as LogAggregation[],
};
break;
case 'metrics':
default:
spec = {
name: queryName,
signal: 'metrics' as const,
...baseSpec,
aggregations: aggregations as MetricAggregation[],
// reduceTo: queryData.reduceTo,
};
break;
}
return {
type: 'builder_query' as QueryType,
spec,
};
},
);
}
/**
* Converts PromQL queries to V5 format
*/
function convertPromQueriesToV5(
promQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
): QueryEnvelope[] {
return Object.entries(promQueries).map(
([queryName, queryData]): QueryEnvelope => ({
type: 'promql' as QueryType,
spec: {
name: queryName,
query: queryData.query,
disabled: queryData.disabled || false,
step: queryData.stepInterval,
stats: false, // PromQL specific field
},
}),
);
}
/**
* Converts ClickHouse queries to V5 format
*/
function convertClickHouseQueriesToV5(
chQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
): QueryEnvelope[] {
return Object.entries(chQueries).map(
([queryName, queryData]): QueryEnvelope => ({
type: 'clickhouse_sql' as QueryType,
spec: {
name: queryName,
query: queryData.query,
disabled: queryData.disabled || false,
// ClickHouse doesn't have step or stats like PromQL
},
}),
);
}
/**
* Converts query formulas to V5 format
*/
function convertFormulasToV5(
formulas: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
): QueryEnvelope[] {
return Object.entries(formulas).map(
([queryName, formulaData]): QueryEnvelope => ({
type: 'builder_formula' as QueryType,
spec: {
name: queryName,
expression: formulaData.expression || '',
functions: formulaData.functions,
},
}),
);
}
/**
* Helper function to reduce query arrays to objects
*/
function reduceQueriesToObject(
queryArray: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
): { queries: Record<string, any>; legends: Record<string, string> } {
// eslint-disable-line @typescript-eslint/no-explicit-any
const legends: Record<string, string> = {};
const queries = queryArray.reduce((acc, queryItem) => {
if (!queryItem.query) return acc;
acc[queryItem.name] = queryItem;
legends[queryItem.name] = queryItem.legend;
return acc;
}, {} as Record<string, any>); // eslint-disable-line @typescript-eslint/no-explicit-any
return { queries, legends };
}
/**
* Prepares V5 query range payload from GetQueryResultsProps
*/
export const prepareQueryRangePayloadV5 = ({
query,
globalSelectedInterval,
graphType,
selectedTime,
tableParams,
variables = {},
start: startTime,
end: endTime,
formatForWeb,
}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => {
let legendMap: Record<string, string> = {};
const requestType = mapPanelTypeToRequestType(graphType);
let queries: QueryEnvelope[] = [];
switch (query.queryType) {
case EQueryType.QUERY_BUILDER: {
const { queryData: data, queryFormulas } = query.builder;
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
// Combine legend maps
legendMap = {
...currentQueryData.newLegendMap,
...currentFormulas.newLegendMap,
};
// Convert builder queries
const builderQueries = convertBuilderQueriesToV5(
currentQueryData.data,
requestType,
graphType,
);
// Convert formulas as separate query type
const formulaQueries = convertFormulasToV5(currentFormulas.data);
// Combine both types
queries = [...builderQueries, ...formulaQueries];
break;
}
case EQueryType.PROM: {
const promQueries = reduceQueriesToObject(query[query.queryType]);
queries = convertPromQueriesToV5(promQueries.queries);
legendMap = promQueries.legends;
break;
}
case EQueryType.CLICKHOUSE: {
const chQueries = reduceQueriesToObject(query[query.queryType]);
queries = convertClickHouseQueriesToV5(chQueries.queries);
legendMap = chQueries.legends;
break;
}
default:
break;
}
// Calculate time range
const { start, end } = getStartEndRangeTime({
type: selectedTime,
interval: globalSelectedInterval,
});
// Create V5 payload
const queryPayload: QueryRangePayloadV5 = {
schemaVersion: 'v1',
start: startTime ? startTime * 1e3 : parseInt(start, 10) * 1e3,
end: endTime ? endTime * 1e3 : parseInt(end, 10) * 1e3,
requestType,
compositeQuery: {
queries,
},
formatOptions: {
formatTableResultForUI: !!formatForWeb,
},
variables: Object.entries(variables).reduce((acc, [key, value]) => {
acc[key] = { value };
return acc;
}, {} as Record<string, VariableItem>),
};
return { legendMap, queryPayload };
};

View File

@@ -1,8 +0,0 @@
// V5 API exports
export * from './queryRange/constants';
export { convertV5ResponseToLegacy } from './queryRange/convertV5Response';
export { getQueryRangeV5 } from './queryRange/getQueryRange';
export { prepareQueryRangePayloadV5 } from './queryRange/prepareQueryRangePayloadV5';
// Export types from proper location
export * from 'types/api/v5/queryRange';

View File

@@ -1,161 +0,0 @@
.changelog-modal {
.ant-modal-content {
padding: unset;
background-color: var(--bg-ink-400, #121317);
.ant-modal-header {
margin-bottom: unset;
}
.ant-modal-footer {
margin-top: unset;
}
}
&-title {
display: flex;
align-items: center;
gap: 8px;
background-color: var(--bg-ink-400, #121317);
padding: 16px;
font-size: 14px;
line-height: 20px;
color: var(--text-vanilla-100, #fff);
border-bottom: 1px solid var(--bg-slate-500, #161922);
}
&-footer.scroll-available {
.scroll-btn-container {
display: block;
}
}
&-footer {
position: relative;
border: 1px solid var(--bg-slate-500, #161922);
padding: 12px;
display: flex;
align-items: center;
justify-content: space-between;
&-label {
color: var(--text-robin-400, #7190f9);
font-size: 14px;
line-height: 24px;
position: relative;
padding-left: 14px;
&::before {
content: '';
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
width: 6px;
height: 6px;
border-radius: 100%;
background-color: var(--bg-robin-500, #7190f9);
}
}
&-ctas {
display: flex;
& svg {
font-size: 14px;
}
}
.scroll-btn-container {
display: none;
position: absolute;
top: -40px;
left: 50%;
transform: translateX(-50%);
.scroll-btn {
all: unset;
padding: 4px 12px 4px 10px;
background-color: var(--bg-slate-400, #1d212d);
border-radius: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
transition: background-color 0.1s;
&:hover {
background-color: var(--bg-slate-200, #2c3140);
}
&:active {
background-color: var(--bg-slate-600, #1c1f2a);
}
span {
font-size: 12px;
line-height: 18px;
color: var(--text-vanilla-400, #c0c1c3);
}
// add animation to the chevrons down icon
svg {
animation: pulse 1s infinite;
}
}
}
}
&-content {
max-height: calc(100vh - 300px);
overflow-y: auto;
padding: 16px;
border: 1px solid var(--bg-slate-500, #161922);
border-top-width: 0;
border-bottom-width: 0;
}
}
// pulse for the scroll for more icon
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.lightMode {
.changelog-modal {
.ant-modal-content {
background-color: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
}
&-title {
background: var(--bg-vanilla-100);
color: var(--bg-ink-500);
border-color: var(--bg-vanilla-300);
}
&-content {
border-color: var(--bg-vanilla-300);
}
&-footer {
border-color: var(--bg-vanilla-300);
.scroll-btn-container {
.scroll-btn {
background-color: var(--bg-vanilla-300);
span {
color: var(--text-ink-500);
}
}
}
}
}
}

View File

@@ -1,131 +0,0 @@
import './ChangelogModal.styles.scss';
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
import { Button, Modal } from 'antd';
import cx from 'classnames';
import dayjs from 'dayjs';
import { ChevronsDown, ScrollText } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useRef, useState } from 'react';
import ChangelogRenderer from './components/ChangelogRenderer';
interface Props {
onClose: () => void;
}
function ChangelogModal({ onClose }: Props): JSX.Element {
const [hasScroll, setHasScroll] = useState(false);
const changelogContentSectionRef = useRef<HTMLDivElement>(null);
const { changelog } = useAppContext();
const formattedReleaseDate = dayjs(changelog?.release_date).format(
'MMMM D, YYYY',
);
const checkScroll = useCallback((): void => {
if (changelogContentSectionRef.current) {
const {
scrollHeight,
clientHeight,
scrollTop,
} = changelogContentSectionRef.current;
const isAtBottom = scrollHeight - clientHeight - scrollTop <= 8;
setHasScroll(scrollHeight > clientHeight + 24 && !isAtBottom); // 24px - buffer height to show show more
}
}, []);
useEffect(() => {
checkScroll();
const changelogContentSection = changelogContentSectionRef.current;
if (changelogContentSection) {
changelogContentSection.addEventListener('scroll', checkScroll);
}
return (): void => {
if (changelogContentSection) {
changelogContentSection.removeEventListener('scroll', checkScroll);
}
};
}, [checkScroll]);
const onClickUpdateWorkspace = (): void => {
window.open(
'https://github.com/SigNoz/signoz/releases',
'_blank',
'noopener,noreferrer',
);
};
const onClickScrollForMore = (): void => {
if (changelogContentSectionRef.current) {
changelogContentSectionRef.current.scrollTo({
top: changelogContentSectionRef.current.scrollTop + 600, // Scroll 600px from the current position
behavior: 'smooth',
});
}
};
return (
<Modal
className={cx('changelog-modal')}
title={
<div className="changelog-modal-title">
<ScrollText size={16} />
Whats New Changelog : {formattedReleaseDate}
</div>
}
width={820}
open
onCancel={onClose}
footer={
<div
className={cx('changelog-modal-footer', hasScroll && 'scroll-available')}
>
{changelog?.features && changelog.features.length > 0 && (
<span className="changelog-modal-footer-label">
{changelog.features.length} new&nbsp;
{changelog.features.length > 1 ? 'features' : 'feature'}
</span>
)}
<div className="changelog-modal-footer-ctas">
<Button type="default" icon={<CloseOutlined />} onClick={onClose}>
Skip for now
</Button>
<Button
type="primary"
icon={<CheckOutlined />}
onClick={onClickUpdateWorkspace}
>
Update my workspace
</Button>
</div>
{changelog && (
<div className="scroll-btn-container">
<button
data-testid="scroll-more-btn"
type="button"
className="scroll-btn"
onClick={onClickScrollForMore}
>
<ChevronsDown size={14} />
<span>Scroll for more</span>
</button>
</div>
)}
</div>
}
>
<div
className="changelog-modal-content"
data-testid="changelog-content"
ref={changelogContentSectionRef}
>
{changelog && <ChangelogRenderer changelog={changelog} />}
</div>
</Modal>
);
}
export default ChangelogModal;

View File

@@ -1,79 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable sonarjs/no-identical-functions */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { fireEvent, render, screen } from '@testing-library/react';
import ChangelogModal from '../ChangelogModal';
const mockChangelog = {
release_date: '2025-06-10',
features: [
{
id: 1,
title: 'Feature 1',
description: 'Description for feature 1',
media: null,
},
],
bug_fixes: 'Bug fix details',
maintenance: 'Maintenance details',
};
// Mock react-markdown to just render children as plain text
jest.mock(
'react-markdown',
() =>
function ReactMarkdown({ children }: any) {
return <div>{children}</div>;
},
);
// mock useAppContext
jest.mock('providers/App/App', () => ({
useAppContext: jest.fn(() => ({ changelog: mockChangelog })),
}));
describe('ChangelogModal', () => {
it('renders modal with changelog data', () => {
render(<ChangelogModal onClose={jest.fn()} />);
expect(
screen.getByText('Whats New ⎯ Changelog : June 10, 2025'),
).toBeInTheDocument();
expect(screen.getByText('Feature 1')).toBeInTheDocument();
expect(screen.getByText('Description for feature 1')).toBeInTheDocument();
expect(screen.getByText('Bug fix details')).toBeInTheDocument();
expect(screen.getByText('Maintenance details')).toBeInTheDocument();
});
it('calls onClose when Skip for now is clicked', () => {
const onClose = jest.fn();
render(<ChangelogModal onClose={onClose} />);
fireEvent.click(screen.getByText('Skip for now'));
expect(onClose).toHaveBeenCalled();
});
it('opens migration docs when Update my workspace is clicked', () => {
window.open = jest.fn();
render(<ChangelogModal onClose={jest.fn()} />);
fireEvent.click(screen.getByText('Update my workspace'));
expect(window.open).toHaveBeenCalledWith(
'https://github.com/SigNoz/signoz/releases',
'_blank',
'noopener,noreferrer',
);
});
it('scrolls for more when Scroll for more is clicked', () => {
render(<ChangelogModal onClose={jest.fn()} />);
const scrollBtn = screen.getByTestId('scroll-more-btn');
const contentDiv = screen.getByTestId('changelog-content');
if (contentDiv) {
contentDiv.scrollTo = jest.fn();
}
fireEvent.click(scrollBtn);
if (contentDiv) {
expect(contentDiv.scrollTo).toHaveBeenCalled();
}
});
});

View File

@@ -1,63 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable sonarjs/no-identical-functions */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { render, screen } from '@testing-library/react';
import ChangelogRenderer from '../components/ChangelogRenderer';
// Mock react-markdown to just render children as plain text
jest.mock(
'react-markdown',
() =>
function ReactMarkdown({ children }: any) {
return <div>{children}</div>;
},
);
const mockChangelog = {
id: 1,
documentId: 'changelog-doc-1',
version: '1.0.0',
createdAt: '2025-06-09T12:00:00Z',
release_date: '2025-06-10',
features: [
{
id: 1,
documentId: '1',
title: 'Feature 1',
description: 'Description for feature 1',
sort_order: 1,
createdAt: '',
updatedAt: '',
publishedAt: '',
deployment_type: 'All',
media: {
id: 1,
documentId: 'doc1',
ext: '.webp',
url: '/uploads/feature1.webp',
mime: 'image/webp',
alternativeText: null,
},
},
],
bug_fixes: 'Bug fix details',
updatedAt: '2025-06-09T12:00:00Z',
publishedAt: '2025-06-09T12:00:00Z',
maintenance: 'Maintenance details',
};
describe('ChangelogRenderer', () => {
it('renders release date', () => {
render(<ChangelogRenderer changelog={mockChangelog} />);
expect(screen.getByText('June 10, 2025')).toBeInTheDocument();
});
it('renders features, media, and description', () => {
render(<ChangelogRenderer changelog={mockChangelog} />);
expect(screen.getByText('Feature 1')).toBeInTheDocument();
expect(screen.getByAltText('Media')).toBeInTheDocument();
expect(screen.getByText('Description for feature 1')).toBeInTheDocument();
});
});

View File

@@ -1,136 +0,0 @@
.changelog-renderer {
position: relative;
padding-left: 20px;
.changelog-release-date {
font-size: 14px;
line-height: 20px;
color: var(--text-vanilla-400, #c0c1c3);
}
&-list {
display: flex;
flex-direction: column;
gap: 28px;
}
&-line {
position: absolute;
left: 0;
top: 6px;
bottom: -30px;
width: 1px;
background-color: var(--bg-slate-400, #1d212d);
.inner-ball {
position: absolute;
left: 50%;
width: 6px;
height: 6px;
border-radius: 100%;
transform: translateX(-50%);
background-color: var(--bg-robin-500, #7190f9);
}
}
ul,
ol {
list-style: none;
display: flex;
flex-direction: column;
gap: 16px;
padding-left: 30px;
li {
position: relative;
&::before {
content: '';
position: absolute;
left: -10px;
top: 10px;
width: 20px;
height: 2px;
background-color: var(--bg-robin-500, #7190f9);
transform: translate(-100%, -50%);
}
}
}
li,
p {
font-size: 14px;
line-height: 20px;
color: var(--text-vanilla-400, #c0c1c3);
}
code {
padding: 2px 4px;
background-color: var(--bg-slate-500, #161922);
border-radius: 6px;
font-size: 95%;
vertical-align: middle;
border: 1px solid var(--bg-slate-600, #1c1f2a);
}
a {
color: var(--text-robin-500, #7190f9);
font-weight: 600;
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 600;
color: var(--text-vanilla-100, #fff);
}
h1 {
font-size: 24px;
line-height: 32px;
}
h2 {
font-size: 20px;
line-height: 28px;
}
.changelog-media-image {
height: auto;
width: 100%;
overflow: hidden;
border-radius: 4px;
border: 1px solid var(--bg-slate-400, #1d212d);
}
}
.lightMode {
.changelog-renderer {
.changelog-release-date {
color: var(--text-ink-500);
}
&-line {
background-color: var(--bg-vanilla-300);
}
li,
p {
color: var(--text-ink-500);
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--text-ink-500);
}
}
}

View File

@@ -1,89 +0,0 @@
import './ChangelogRenderer.styles.scss';
import dayjs from 'dayjs';
import ReactMarkdown from 'react-markdown';
import {
ChangelogSchema,
Media,
SupportedImageTypes,
SupportedVideoTypes,
} from 'types/api/changelog/getChangelogByVersion';
interface Props {
changelog: ChangelogSchema;
}
function renderMedia(media: Media): JSX.Element | null {
if (SupportedImageTypes.includes(media.ext)) {
return (
<img
src={media.url}
alt={media.alternativeText || 'Media'}
width={800}
height={450}
className="changelog-media-image"
/>
);
}
if (SupportedVideoTypes.includes(media.ext)) {
return (
<video
autoPlay
controls
controlsList="nodownload noplaybackrate"
loop
className="my-3 h-auto w-full rounded"
>
<source src={media.url} type={media.mime} />
<track kind="captions" src="" label="No captions available" default />
Your browser does not support the video tag.
</video>
);
}
return null;
}
function ChangelogRenderer({ changelog }: Props): JSX.Element {
const formattedReleaseDate = dayjs(changelog.release_date).format(
'MMMM D, YYYY',
);
return (
<div className="changelog-renderer">
<div className="changelog-renderer-line">
<div className="inner-ball" />
</div>
<span className="changelog-release-date">{formattedReleaseDate}</span>
{changelog.features && changelog.features.length > 0 && (
<div className="changelog-renderer-list flex flex-col gap-7">
{changelog.features.map((feature) => (
<div key={feature.id}>
<h2>{feature.title}</h2>
{feature.media && renderMedia(feature.media)}
<ReactMarkdown>{feature.description}</ReactMarkdown>
</div>
))}
</div>
)}
{changelog.bug_fixes && changelog.bug_fixes.length > 0 && (
<div>
<h2>Bug Fixes</h2>
{changelog.bug_fixes && (
<ReactMarkdown>{changelog.bug_fixes}</ReactMarkdown>
)}
</div>
)}
{changelog.maintenance && changelog.maintenance.length > 0 && (
<div>
<h2>Maintenance</h2>
{changelog.maintenance && (
<ReactMarkdown>{changelog.maintenance}</ReactMarkdown>
)}
</div>
)}
</div>
);
}
export default ChangelogRenderer;

View File

@@ -74,7 +74,6 @@ const formatMap = {
'MM/dd HH:mm': DATE_TIME_FORMATS.SLASH_SHORT,
'MM/DD': DATE_TIME_FORMATS.DATE_SHORT,
'YY-MM': DATE_TIME_FORMATS.YEAR_MONTH,
'MMM d, yyyy, h:mm:ss aaaa': DATE_TIME_FORMATS.DASH_DATETIME,
YY: DATE_TIME_FORMATS.YEAR_SHORT,
};

View File

@@ -169,7 +169,6 @@
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.ant-drawer-close {
padding: 0px;
}

View File

@@ -430,13 +430,9 @@ function HostMetricsDetails({
>
{host.active ? 'ACTIVE' : 'INACTIVE'}
</Tag>
{host.os ? (
<Tag className="infra-monitoring-tags" bordered>
{host.os}
</Tag>
) : (
<Typography.Text>-</Typography.Text>
)}
<Tag className="infra-monitoring-tags" bordered>
{host.os}
</Tag>
<div className="progress-container">
<Progress
percent={Number((host.cpu * 100).toFixed(1))}

View File

@@ -1,101 +0,0 @@
.input-with-label {
display: flex;
flex-direction: row;
border-radius: 2px 0px 0px 2px;
.label {
color: var(--bg-vanilla-400);
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 128.571% */
letter-spacing: 0.56px;
max-width: 150px;
min-width: 120px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0px 8px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
display: flex;
justify-content: flex-start;
align-items: center;
font-weight: var(--font-weight-light);
}
.input {
flex: 1;
min-width: 150px;
font-family: 'Space Mono', monospace !important;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
border-right: none;
border-left: none;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
height: 38px;
width: 38px;
}
&.labelAfter {
.input {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}
.label {
border-left: none;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
}
}
.lightMode {
.input-with-label {
.label {
color: var(--bg-ink-500) !important;
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
.input {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
.close-btn {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
&.labelAfter {
.input {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
}
}
}

View File

@@ -1,71 +0,0 @@
import './InputWithLabel.styles.scss';
import { Button, Input, Typography } from 'antd';
import cx from 'classnames';
import { X } from 'lucide-react';
import { useState } from 'react';
function InputWithLabel({
label,
initialValue,
placeholder,
type,
onClose,
labelAfter,
onChange,
className,
}: {
label: string;
initialValue?: string | number;
placeholder: string;
type?: string;
onClose?: () => void;
labelAfter?: boolean;
onChange: (value: string) => void;
className?: string;
}): JSX.Element {
const [inputValue, setInputValue] = useState<string>(
initialValue ? initialValue.toString() : '',
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setInputValue(e.target.value);
onChange?.(e.target.value);
};
return (
<div
className={cx('input-with-label', className, {
labelAfter,
})}
>
{!labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
<Input
className="input"
placeholder={placeholder}
type={type}
value={inputValue}
onChange={handleChange}
name={label.toLowerCase()}
/>
{labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
{onClose && (
<Button
className="periscope-btn ghost close-btn"
icon={<X size={16} />}
onClick={onClose}
/>
)}
</div>
);
}
InputWithLabel.defaultProps = {
type: 'text',
onClose: undefined,
labelAfter: false,
initialValue: undefined,
className: undefined,
};
export default InputWithLabel;

View File

@@ -5,19 +5,17 @@ import cx from 'classnames';
import { OPERATORS } from 'constants/queryBuilder';
import { FontSize } from 'container/OptionsMenu/types';
import { memo, MouseEvent, ReactNode, useMemo } from 'react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
function AddToQueryHOC({
fieldKey,
fieldValue,
onAddToQuery,
fontSize,
dataType = DataTypes.EMPTY,
children,
}: AddToQueryHOCProps): JSX.Element {
const handleQueryAdd = (event: MouseEvent<HTMLDivElement>): void => {
event.stopPropagation();
onAddToQuery(fieldKey, fieldValue, OPERATORS['='], undefined, dataType);
onAddToQuery(fieldKey, fieldValue, OPERATORS['=']);
};
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
@@ -37,20 +35,9 @@ function AddToQueryHOC({
export interface AddToQueryHOCProps {
fieldKey: string;
fieldValue: string;
onAddToQuery: (
fieldKey: string,
fieldValue: string,
operator: string,
isJSON?: boolean,
dataType?: DataTypes,
) => void;
onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void;
fontSize: FontSize;
dataType?: DataTypes;
children: ReactNode;
}
AddToQueryHOC.defaultProps = {
dataType: DataTypes.EMPTY,
};
export default memo(AddToQueryHOC);

View File

@@ -20,7 +20,6 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
export const defaultTableStyle: CSSProperties = {
minWidth: '40rem',
maxWidth: '60rem',
};
export const defaultListViewPanelStyle: CSSProperties = {

View File

@@ -410,18 +410,18 @@ export default function LogsFormatOptionsMenu({
)}
<div className="column-format">
{addColumn?.value?.map(({ name }) => (
<div className="column-name" key={name}>
{addColumn?.value?.map(({ key, id }) => (
<div className="column-name" key={id}>
<div className="name">
<Tooltip placement="left" title={name}>
{name}
<Tooltip placement="left" title={key}>
{key}
</Tooltip>
</div>
{addColumn?.value?.length > 1 && (
<X
className="delete-btn"
size={14}
onClick={(): void => addColumn.onRemove(name)}
onClick={(): void => addColumn.onRemove(id as string)}
/>
)}
</div>

View File

@@ -1,553 +0,0 @@
.query-builder-v2 {
display: flex;
flex-direction: row;
gap: 4px;
width: 100%;
border-bottom: 1px solid var(--bg-slate-400);
border-top: 1px solid var(--bg-slate-400);
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', sans-serif;
border-right: none;
border-left: none;
.qb-content-container {
display: flex;
flex-direction: column;
width: calc(100% - 44px);
flex: 1;
position: relative;
}
.qb-content-section {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
flex: 1;
.qb-header-container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-left: 32px;
.query-actions-container {
display: flex;
flex-direction: row;
gap: 8px;
justify-content: space-between;
align-items: center;
width: 100%;
}
}
.qb-elements-container {
display: flex;
flex-direction: column;
gap: 8px;
margin-left: 108px;
.code-mirror-where-clause,
.query-aggregation-container,
.query-add-ons,
.metrics-aggregation-section-content {
position: relative;
&::before {
content: '';
position: absolute;
left: -10px;
top: 12px;
width: 6px;
height: 6px;
border-left: 6px dotted #1d212d;
}
/* Horizontal line pointing from vertical to the item */
&::after {
content: '';
position: absolute;
left: -28px;
top: 15px;
width: 24px;
height: 1px;
background: repeating-linear-gradient(
to right,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
}
}
}
}
.where-clause-view {
.qb-content-section {
.qb-elements-container {
margin-left: 0px;
.code-mirror-where-clause,
.query-aggregation-container,
.query-add-ons,
.metrics-aggregation-section-content {
&::before {
display: none;
}
&::after {
display: none;
}
}
}
}
}
.query-names-section {
display: flex;
flex-direction: column;
gap: 8px;
width: 44px;
padding: 8px;
border-left: 1px solid var(--bg-slate-400);
.query-name {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10px;
padding: 4px;
border-radius: 0px 2px 2px 0px;
border-radius: 2px;
border: 1px solid rgba(242, 71, 105, 0.2);
background: rgba(242, 71, 105, 0.1);
color: var(--Sakura-400, #f56c87);
font-family: 'Space Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 128.571% */
text-transform: uppercase;
}
.formula-name {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10px;
padding: 4px;
border-radius: 0px 2px 2px 0px;
border-radius: 2px;
border: 1px solid rgba(173, 127, 88, 0.2);
background: rgba(173, 127, 88, 0.1);
color: var(--Sienna-500, #ad7f58);
font-family: 'Space Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 128.571% */
text-transform: uppercase;
}
}
.qb-formulas-container {
display: flex;
flex-direction: column;
gap: 8px;
margin-left: 32px;
padding-bottom: 16px;
padding-left: 8px;
.qb-formula {
.ant-row {
row-gap: 0px !important;
}
.qb-entity-options {
margin-left: 8px;
padding-right: 8px;
}
.formula-container {
margin-left: 82px;
padding: 4px 0px;
.ant-col {
&::before {
content: '';
position: absolute;
left: -10px;
top: 12px;
width: 6px;
height: 6px;
border-left: 6px dotted #1d212d;
}
/* Horizontal line pointing from vertical to the item */
&::after {
content: '';
position: absolute;
left: -28px;
top: 15px;
width: 24px;
height: 1px;
background: repeating-linear-gradient(
to right,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
}
}
.formula-expression {
border-bottom-left-radius: 0px !important;
border-bottom-right-radius: 0px !important;
font-family: 'Space Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 128.571% */
resize: none;
}
.formula-legend {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
.ant-input-group-addon {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
}
.ant-input {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
}
}
}
}
}
.qb-footer {
padding: 0 8px 16px 8px;
.qb-footer-container {
display: flex;
flex-direction: row;
gap: 8px;
margin-left: 32px;
.qb-add-new-query {
display: flex;
flex-direction: row;
gap: 8px;
&::before {
content: '';
height: calc(100% - 82px);
content: '';
position: absolute;
left: 56px;
top: 31px;
bottom: 0;
width: 1px;
background: repeating-linear-gradient(
to bottom,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
}
}
}
}
.qb-entity-options {
display: flex;
flex-direction: row;
gap: 8px;
.options {
.query-name {
border-radius: 0px 2px 2px 0px !important;
border: 1px solid rgba(242, 71, 105, 0.2) !important;
background: rgba(242, 71, 105, 0.1) !important;
color: var(--Sakura-400, #f56c87) !important;
font-family: 'Space Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
text-transform: uppercase;
&::before {
content: '';
height: 120px;
content: '';
position: absolute;
left: 0;
top: 31px;
bottom: 0;
width: 1px;
background: repeating-linear-gradient(
to bottom,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
left: 15px;
}
}
.formula-name {
border-radius: 0px 2px 2px 0px;
border: 1px solid rgba(173, 127, 88, 0.2);
background: rgba(173, 127, 88, 0.1);
font-family: 'Space Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
text-transform: uppercase;
&::before {
content: '';
height: 65px;
content: '';
position: absolute;
left: 0;
top: 31px;
bottom: 0;
width: 1px;
background: repeating-linear-gradient(
to bottom,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
left: 15px;
}
}
}
.query-data-source {
margin-left: 8px;
.ant-select-selector {
min-width: 120px;
border-radius: 2px;
border: 1px solid var(--Slate-400, #1d212d);
background: var(--Ink-300, #16181d);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
}
.qb-search-container {
.metrics-select-container {
margin-bottom: 12px;
}
}
.qb-search-filter-container {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 8px;
flex-wrap: wrap;
.query-search-container {
flex: 1;
}
.traces-search-filter-container {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
width: 180px;
}
.ant-select {
height: auto;
}
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--Slate-400, #1d212d) !important;
background: var(--Ink-300, #16181d) !important;
height: 34px !important;
box-sizing: border-box !important;
}
.ant-select-arrow {
color: var(--bg-vanilla-400) !important;
}
}
.query-actions-dropdown {
cursor: pointer;
}
}
.lightMode {
.query-builder-v2 {
border-bottom: 1px solid var(--bg-vanilla-300);
border-top: 1px solid var(--bg-vanilla-300);
.qb-content-section {
.qb-elements-container {
.code-mirror-where-clause,
.query-aggregation-container,
.query-add-ons,
.metrics-aggregation-section-content {
&::before {
border-left: 6px dotted var(--bg-vanilla-300);
}
/* Horizontal line pointing from vertical to the item */
&::after {
background: repeating-linear-gradient(
to right,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
}
}
}
.query-names-section {
border-left: 1px solid var(--bg-vanilla-300);
}
.qb-formulas-container {
.qb-formula {
.formula-container {
.ant-col {
&::before {
border-left: 6px dotted var(--bg-vanilla-300);
}
/* Horizontal line pointing from vertical to the item */
&::after {
background: repeating-linear-gradient(
to right,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
}
}
}
}
.qb-footer {
.qb-footer-container {
.qb-add-new-query {
&::before {
background: repeating-linear-gradient(
to bottom,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
}
}
}
.qb-entity-options {
.options {
.query-name {
&::before {
background: repeating-linear-gradient(
to bottom,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
}
.formula-name {
&::before {
background: repeating-linear-gradient(
to bottom,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
left: 15px;
}
}
}
.query-data-source {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1) !important;
}
}
}
.qb-search-filter-container {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1) !important;
}
.ant-select-arrow {
color: var(--bg-vanilla-400) !important;
}
}
}
}

View File

@@ -1,185 +0,0 @@
import './QueryBuilderV2.styles.scss';
import { OPERATORS, PANEL_TYPES } from 'constants/queryBuilder';
import { Formula } from 'container/QueryBuilder/components/Formula';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo, useEffect, useMemo, useRef } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
import { QueryV2 } from './QueryV2/QueryV2';
export const QueryBuilderV2 = memo(function QueryBuilderV2({
config,
panelType: newPanelType,
filterConfigs = {},
queryComponents,
isListViewPanel = false,
showOnlyWhereClause = false,
version,
}: QueryBuilderProps): JSX.Element {
const {
currentQuery,
addNewBuilderQuery,
addNewFormula,
handleSetConfig,
panelType,
initialDataSource,
} = useQueryBuilder();
const containerRef = useRef(null);
const currentDataSource = useMemo(
() =>
(config && config.queryVariant === 'static' && config.initialDataSource) ||
null,
[config],
);
useEffect(() => {
if (currentDataSource !== initialDataSource || newPanelType !== panelType) {
if (newPanelType === PANEL_TYPES.BAR) {
handleSetConfig(PANEL_TYPES.BAR, DataSource.METRICS);
return;
}
handleSetConfig(newPanelType, currentDataSource);
}
}, [
handleSetConfig,
panelType,
initialDataSource,
currentDataSource,
newPanelType,
]);
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: true, isDisabled: true },
having: { isHidden: true, isDisabled: true },
filters: {
customKey: 'body',
customOp: OPERATORS.CONTAINS,
},
};
return config;
}, []);
const listViewTracesFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: true, isDisabled: true },
having: { isHidden: true, isDisabled: true },
limit: { isHidden: true, isDisabled: true },
filters: {
customKey: 'body',
customOp: OPERATORS.CONTAINS,
},
};
return config;
}, []);
const queryFilterConfigs = useMemo(() => {
if (isListViewPanel) {
return currentQuery.builder.queryData[0].dataSource === DataSource.TRACES
? listViewTracesFilterConfigs
: listViewLogFilterConfigs;
}
return filterConfigs;
}, [
isListViewPanel,
filterConfigs,
currentQuery.builder.queryData,
listViewLogFilterConfigs,
listViewTracesFilterConfigs,
]);
return (
<QueryBuilderV2Provider>
<div className="query-builder-v2">
<div className="qb-content-container">
{isListViewPanel && (
<QueryV2
ref={containerRef}
key={currentQuery.builder.queryData[0].queryName}
index={0}
query={currentQuery.builder.queryData[0]}
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
version={version}
isAvailableToDisable={false}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
/>
)}
{!isListViewPanel &&
currentQuery.builder.queryData.map((query, index) => (
<QueryV2
ref={containerRef}
key={query.queryName}
index={index}
query={query}
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
version={version}
isAvailableToDisable={false}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
/>
))}
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
<div className="qb-formulas-container">
{currentQuery.builder.queryFormulas.map((formula, index) => {
const query =
currentQuery.builder.queryData[index] ||
currentQuery.builder.queryData[0];
return (
<div key={formula.queryName} className="qb-formula">
<Formula
filterConfigs={filterConfigs}
query={query}
formula={formula}
index={index}
isAdditionalFilterEnable={false}
/>
</div>
);
})}
</div>
)}
{!showOnlyWhereClause && !isListViewPanel && (
<QueryFooter
addNewBuilderQuery={addNewBuilderQuery}
addNewFormula={addNewFormula}
/>
)}
</div>
{!showOnlyWhereClause && !isListViewPanel && (
<div className="query-names-section">
{currentQuery.builder.queryData.map((query) => (
<div key={query.queryName} className="query-name">
{query.queryName}
</div>
))}
{currentQuery.builder.queryFormulas.map((formula) => (
<div key={formula.queryName} className="formula-name">
{formula.queryName}
</div>
))}
</div>
)}
</div>
</QueryBuilderV2Provider>
);
});

View File

@@ -1,62 +0,0 @@
import { createContext, ReactNode, useContext, useMemo, useState } from 'react';
// Types for the context state
export type AggregationOption = { func: string; arg: string };
interface QueryBuilderV2ContextType {
searchText: string;
setSearchText: (text: string) => void;
aggregationOptions: AggregationOption[];
setAggregationOptions: (options: AggregationOption[]) => void;
aggregationInterval: string;
setAggregationInterval: (interval: string) => void;
queryAddValues: any; // Replace 'any' with a more specific type if available
setQueryAddValues: (values: any) => void;
}
const QueryBuilderV2Context = createContext<
QueryBuilderV2ContextType | undefined
>(undefined);
export function QueryBuilderV2Provider({
children,
}: {
children: ReactNode;
}): JSX.Element {
const [searchText, setSearchText] = useState('');
const [aggregationOptions, setAggregationOptions] = useState<
AggregationOption[]
>([]);
const [aggregationInterval, setAggregationInterval] = useState('');
const [queryAddValues, setQueryAddValues] = useState<any>(null); // Replace 'any' if you have a type
return (
<QueryBuilderV2Context.Provider
value={useMemo(
() => ({
searchText,
setSearchText,
aggregationOptions,
setAggregationOptions,
aggregationInterval,
setAggregationInterval,
queryAddValues,
setQueryAddValues,
}),
[searchText, aggregationOptions, aggregationInterval, queryAddValues],
)}
>
{children}
</QueryBuilderV2Context.Provider>
);
}
export const useQueryBuilderV2Context = (): QueryBuilderV2ContextType => {
const context = useContext(QueryBuilderV2Context);
if (!context) {
throw new Error(
'useQueryBuilderV2Context must be used within a QueryBuilderV2Provider',
);
}
return context;
};

View File

@@ -1,97 +0,0 @@
.metrics-aggregate-section {
display: flex;
flex-direction: column;
gap: 16px;
margin: 4px 0;
.metrics-time-aggregation-section {
display: flex;
flex-direction: column;
gap: 12px;
.metrics-time-aggregation-section-title {
display: flex;
align-items: center;
gap: 6px;
color: var(--Slate-50, #62687c);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 150% */
letter-spacing: 0.48px;
}
}
.metrics-space-aggregation-section {
display: flex;
flex-direction: column;
gap: 4px;
.metrics-space-aggregation-section-title {
display: flex;
align-items: center;
gap: 6px;
color: var(--Slate-50, #62687c);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 150% */
letter-spacing: 0.48px;
}
}
.metrics-aggregation-section-content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
.metrics-aggregation-section-content-item {
display: flex;
align-items: center;
gap: 10px;
.metrics-aggregation-section-content-item-label {
color: var(--Vanilla-400, #c0c1c3);
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.metrics-aggregation-section-content-item-value {
min-width: 320px;
.ant-select {
width: 100%;
}
.ant-select-selector {
border-radius: 2px;
border: 1.005px solid var(--Slate-400, #1d212d);
background: var(--Ink-300, #16181d);
}
}
}
}
}
.metrics-operators-select {
border-radius: 2px;
border: 1.005px solid var(--Slate-400, #1d212d);
background: var(--Ink-300, #16181d);
color: var(--Vanilla-400, #c0c1c3);
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}

View File

@@ -1,168 +0,0 @@
import './MetricsAggregateSection.styles.scss';
import { Tooltip } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { ATTRIBUTE_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import SpaceAggregationOptions from 'container/QueryBuilder/components/SpaceAggregationOptions/SpaceAggregationOptions';
import { GroupByFilter, OperatorsSelect } from 'container/QueryBuilder/filters';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Info } from 'lucide-react';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { useQueryBuilderV2Context } from '../../QueryBuilderV2Context';
const MetricsAggregateSection = memo(function MetricsAggregateSection({
query,
index,
version,
panelType,
}: {
query: IBuilderQuery;
index: number;
version: string;
panelType: PANEL_TYPES | null;
}): JSX.Element {
const { setAggregationOptions } = useQueryBuilderV2Context();
const {
operators,
spaceAggregationOptions,
handleChangeQueryData,
handleChangeOperator,
handleSpaceAggregationChange,
} = useQueryOperations({
index,
query,
entityVersion: version,
});
useEffect(() => {
setAggregationOptions([
{
func: query.spaceAggregation || 'count',
arg: query.aggregateAttribute.key || '',
},
]);
}, [
query.spaceAggregation,
query.aggregateAttribute.key,
setAggregationOptions,
query,
]);
const handleChangeGroupByKeys = useCallback(
(value: IBuilderQuery['groupBy']) => {
handleChangeQueryData('groupBy', value);
},
[handleChangeQueryData],
);
const handleChangeAggregateEvery = useCallback(
(value: string) => {
handleChangeQueryData('stepInterval', Number(value));
},
[handleChangeQueryData],
);
const showAggregationInterval = useMemo(() => {
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
if (panelType === PANEL_TYPES.VALUE) {
return false;
}
return true;
}, [panelType]);
const disableOperatorSelector =
!query?.aggregateAttribute.key || query?.aggregateAttribute.key === '';
return (
<div className="metrics-aggregate-section">
<div className="metrics-time-aggregation-section">
<div className="metrics-time-aggregation-section-title">
AGGREGATE BY TIME{' '}
<Tooltip title="AGGREGATE BY TIME">
<Info size={12} />
</Tooltip>
</div>
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label">
Align with
</div>
<div className="metrics-aggregation-section-content-item-value">
<OperatorsSelect
value={query.aggregateOperator}
onChange={handleChangeOperator}
operators={operators}
className="metrics-operators-select"
/>
</div>
</div>
{showAggregationInterval && (
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label">
aggregated every
</div>
<div className="metrics-aggregation-section-content-item-value">
<InputWithLabel
onChange={handleChangeAggregateEvery}
label="Seconds"
placeholder="Enter a number"
labelAfter
initialValue={query?.stepInterval ?? undefined}
/>
</div>
</div>
)}
</div>
</div>
<div className="metrics-space-aggregation-section">
<div className="metrics-space-aggregation-section-title">
AGGREGATE LABELS
<Tooltip title="AGGREGATE LABELS">
<Info size={12} />
</Tooltip>
</div>
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-value space-aggregation-select">
<SpaceAggregationOptions
panelType={panelType}
key={`${panelType}${query.spaceAggregation}${query.timeAggregation}`}
aggregatorAttributeType={
query?.aggregateAttribute.type as ATTRIBUTE_TYPES
}
selectedValue={query.spaceAggregation}
disabled={disableOperatorSelector}
onSelect={handleSpaceAggregationChange}
operators={spaceAggregationOptions}
qbVersion="v3"
/>
</div>
</div>
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label">by</div>
<div className="metrics-aggregation-section-content-item-value">
<GroupByFilter
disabled={!query.aggregateAttribute.key}
query={query}
onChange={handleChangeGroupByKeys}
/>
</div>
</div>
</div>
</div>
</div>
);
});
export default MetricsAggregateSection;

View File

@@ -1,42 +0,0 @@
.metrics-select-container {
margin-bottom: 8px;
.ant-select-selector {
width: 100%;
border-radius: 2px;
border: 1px solid #1d212d !important;
background: #16181d;
color: #fff;
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
min-height: 36px;
}
.ant-select-dropdown {
border-radius: 4px;
border: 1px solid var(--bg-slate-400);
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.ant-select-item {
color: #fff;
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
&:hover {
background: rgba(171, 189, 255, 0.04) !important;
}
}
}
}

View File

@@ -1,28 +0,0 @@
import './MetricsSelect.styles.scss';
import { AggregatorFilter } from 'container/QueryBuilder/filters';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { memo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export const MetricsSelect = memo(function MetricsSelect({
query,
index,
version,
}: {
query: IBuilderQuery;
index: number;
version: string;
}): JSX.Element {
const { handleChangeAggregatorAttribute } = useQueryOperations({
index,
query,
entityVersion: version,
});
return (
<div className="metrics-select-container">
<AggregatorFilter onChange={handleChangeAggregatorAttribute} query={query} />
</div>
);
});

View File

@@ -1,375 +0,0 @@
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable sonarjs/cognitive-complexity */
import {
autocompletion,
closeCompletion,
Completion,
CompletionContext,
completionKeymap,
CompletionResult,
startCompletion,
} from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import { copilot } from '@uiw/codemirror-theme-copilot';
import CodeMirror, { EditorView, keymap } from '@uiw/react-codemirror';
import { Button } from 'antd';
import { useQueryBuilderV2Context } from 'components/QueryBuilderV2/QueryBuilderV2Context';
import { X } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
const havingOperators = [
{
label: '=',
value: '=',
},
{
label: '!=',
value: '!=',
},
{
label: '>',
value: '>',
},
{
label: '<',
value: '<',
},
{
label: '>=',
value: '>=',
},
{
label: '<=',
value: '<=',
},
{
label: 'IN',
value: 'IN',
},
{
label: 'NOT_IN',
value: 'NOT_IN',
},
];
const conjunctions = [
{ label: 'AND', value: 'AND ' },
{ label: 'OR', value: 'OR ' },
];
// Custom extension to stop events from propagating to global shortcuts
const stopEventsExtension = EditorView.domEventHandlers({
keydown: (event) => {
// Stop all keyboard events from propagating to global shortcuts
event.stopPropagation();
event.stopImmediatePropagation();
return false; // Important for CM to know you handled it
},
input: (event) => {
event.stopPropagation();
return false;
},
focus: (event) => {
// Ensure focus events don't interfere with global shortcuts
event.stopPropagation();
return false;
},
blur: (event) => {
// Ensure blur events don't interfere with global shortcuts
event.stopPropagation();
return false;
},
});
function HavingFilter({
onClose,
onChange,
queryData,
}: {
onClose: () => void;
onChange: (value: string) => void;
queryData: IBuilderQuery;
}): JSX.Element {
const { aggregationOptions } = useQueryBuilderV2Context();
const [input, setInput] = useState(
queryData?.havingExpression?.expression || '',
);
useEffect(() => {
setInput(queryData?.havingExpression?.expression || '');
}, [queryData?.havingExpression?.expression]);
const [isFocused, setIsFocused] = useState(false);
const editorRef = useRef<EditorView | null>(null);
const [options, setOptions] = useState<{ label: string; value: string }[]>([]);
const handleChange = (value: string): void => {
setInput(value);
onChange(value);
};
useEffect(() => {
if (isFocused && editorRef.current && options.length > 0) {
startCompletion(editorRef.current);
}
}, [isFocused, options]);
// Update options when aggregation options change
useEffect(() => {
const newOptions = [];
for (let i = 0; i < aggregationOptions.length; i++) {
const opt = aggregationOptions[i];
for (let j = 0; j < havingOperators.length; j++) {
const operator = havingOperators[j];
newOptions.push({
label: `${opt.func}(${opt.arg}) ${operator.label}`,
value: `${opt.func}(${opt.arg}) ${operator.label} `,
apply: (
view: EditorView,
completion: { label: string; value: string },
from: number,
to: number,
): void => {
view.dispatch({
changes: { from, to, insert: completion.value },
selection: { anchor: from + completion.value.length },
});
// Trigger value suggestions immediately after operator
setTimeout(() => {
startCompletion(view);
}, 0);
},
});
}
}
setOptions(newOptions);
}, [aggregationOptions]);
// Helper to check if a string is a number
const isNumber = (token: string): boolean => /^-?\d+(\.\d+)?$/.test(token);
// Helper to check if we're after an operator
const isAfterOperator = (tokens: string[]): boolean => {
if (tokens.length === 0) return false;
const lastToken = tokens[tokens.length - 1];
// Check if the last token is exactly an operator or ends with an operator and space
return havingOperators.some((op) => {
const opWithSpace = `${op.value} `;
return lastToken === op.value || lastToken.endsWith(opWithSpace);
});
};
// Helper function for applying completion with space
const applyCompletionWithSpace = (
view: EditorView,
completion: Completion,
from: number,
to: number,
): void => {
const insertValue =
typeof completion.apply === 'string' ? completion.apply : completion.label;
const newText = `${insertValue} `;
const newPos = from + newText.length;
view.dispatch({
changes: { from, to, insert: newText },
selection: { anchor: newPos, head: newPos },
effects: EditorView.scrollIntoView(newPos),
});
};
const havingAutocomplete = useMemo(() => {
// Helper functions for applying completions
const forceCompletion = (view: EditorView): void => {
setTimeout(() => {
if (view) {
startCompletion(view);
}
}, 0);
};
const applyValueCompletion = (
view: EditorView,
completion: Completion,
from: number,
to: number,
): void => {
applyCompletionWithSpace(view, completion, from, to);
forceCompletion(view);
};
const applyOperatorCompletion = (
view: EditorView,
completion: Completion,
from: number,
to: number,
): void => {
const insertValue =
typeof completion.apply === 'string' ? completion.apply : completion.label;
const insertWithSpace = `${insertValue} `;
view.dispatch({
changes: { from, to, insert: insertWithSpace },
selection: { anchor: from + insertWithSpace.length },
});
forceCompletion(view);
};
return autocompletion({
override: [
(context: CompletionContext): CompletionResult | null => {
const text = context.state.sliceDoc(0, context.pos);
const trimmedText = text.trim();
const tokens = trimmedText.split(/\s+/).filter(Boolean);
// Handle empty state when no aggregation options are available
if (options.length === 0) {
return {
from: context.pos,
options: [
{
label:
'No aggregation functions available. Please add aggregation functions first.',
type: 'text',
apply: (): boolean => true,
},
],
};
}
// Close dropdown after operator to allow custom value entry
if (isAfterOperator(tokens)) {
return null;
}
// Hide suggestions while typing a value after an operator
if (
!text.endsWith(' ') &&
tokens.length >= 2 &&
havingOperators.some((op) => op.value === tokens[tokens.length - 2])
) {
return null;
}
// Suggest key/operator pairs and ( for grouping
if (
tokens.length === 0 ||
conjunctions.some((c) => tokens[tokens.length - 1] === c.value.trim()) ||
tokens[tokens.length - 1] === '('
) {
return {
from: context.pos,
options: options.map((opt) => ({
...opt,
apply: applyOperatorCompletion,
})),
};
}
// Show suggestions when typing
if (tokens.length > 0) {
const lastToken = tokens[tokens.length - 1];
const filteredOptions = options.filter((opt) =>
opt.label.toLowerCase().includes(lastToken.toLowerCase()),
);
if (filteredOptions.length > 0) {
return {
from: context.pos - lastToken.length,
options: filteredOptions.map((opt) => ({
...opt,
apply: applyOperatorCompletion,
})),
};
}
}
// Suggest conjunctions after a value and a space
if (
tokens.length > 0 &&
(isNumber(tokens[tokens.length - 1]) ||
tokens[tokens.length - 1] === ')') &&
text.endsWith(' ')
) {
return {
from: context.pos,
options: conjunctions.map((conj) => ({
...conj,
apply: applyValueCompletion,
})),
};
}
// Show all options if no other condition matches
return {
from: context.pos,
options: options.map((opt) => ({
...opt,
apply: applyOperatorCompletion,
})),
};
},
],
defaultKeymap: true,
closeOnBlur: true,
maxRenderedOptions: 200,
activateOnTyping: true,
});
}, [options]);
return (
<div className="having-filter-container">
<div className="having-filter-select-container">
<CodeMirror
value={input}
onChange={handleChange}
theme={copilot}
className="having-filter-select-editor"
width="100%"
extensions={[
havingAutocomplete,
javascript({ jsx: false, typescript: false }),
stopEventsExtension,
EditorView.lineWrapping,
keymap.of([
...completionKeymap,
{
key: 'Escape',
run: closeCompletion,
},
]),
]}
placeholder="Type Having query like count() > 10 ..."
basicSetup={{
lineNumbers: false,
autocompletion: true,
completionKeymap: true,
}}
onCreateEditor={(view: EditorView): void => {
editorRef.current = view;
}}
onFocus={(): void => {
setIsFocused(true);
if (editorRef.current) {
startCompletion(editorRef.current);
}
}}
onBlur={(): void => {
setIsFocused(false);
if (editorRef.current) {
closeCompletion(editorRef.current);
}
}}
/>
<Button
className="close-btn periscope-btn ghost"
icon={<X size={16} />}
onClick={onClose}
/>
</div>
</div>
);
}
export default HavingFilter;

View File

@@ -1,377 +0,0 @@
.add-ons-list {
display: flex;
justify-content: space-between;
align-items: center;
.add-ons-tabs {
display: flex;
flex-wrap: wrap;
.add-on-tab-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
color: var(--Vanilla-400, #c0c1c3);
}
.tab {
border: 1px solid var(--bg-slate-400);
border-left: none;
min-width: 120px;
height: 36px;
line-height: 36px;
&:first-child {
border-left: 1px solid var(--bg-slate-400);
}
}
.tab::before {
background: var(--bg-slate-400);
}
.selected-view {
color: var(--text-robin-500);
border: 1px solid var(--bg-slate-400);
display: none;
}
.selected-view::before {
background: var(--bg-slate-400);
}
}
.compass-button {
width: 30px;
height: 30px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.having-filter-container {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
.having-filter-select-container {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
.having-filter-select-editor {
border-radius: 2px;
flex: 1;
width: calc(100% - 40px);
.cm-content {
padding: 0;
}
.cm-editor {
border-radius: 2px;
background-color: transparent !important;
position: relative !important;
&:focus-within {
border-color: var(--bg-robin-500);
}
&.cm-focused {
outline: none !important;
}
.cm-content {
border-radius: 2px;
border: 1px solid var(--Slate-400, #1d212d);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
padding: 0px !important;
background-color: #121317 !important;
&:focus-within {
border-color: var(--bg-ink-200);
}
}
.cm-tooltip-autocomplete {
background: var(--bg-ink-300) !important;
color: var(--bg-ink-500) !important;
border-radius: 2px !important;
font-size: 12px !important;
font-weight: 500 !important;
margin-top: -2px !important;
width: 100% !important;
position: absolute !important;
top: 38px !important;
left: 0px !important;
border-radius: 4px;
border: 1px solid var(--bg-slate-200, #1d212d);
border-top: none !important;
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
) !important;
backdrop-filter: blur(20px);
box-sizing: border-box;
font-family: 'Space Mono', monospace !important;
ul {
width: 100% !important;
max-width: 100% !important;
font-family: 'Space Mono', monospace !important;
min-height: 200px !important;
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
li {
width: 100% !important;
max-width: 100% !important;
line-height: 36px !important;
height: 36px !important;
padding: 4px 8px !important;
display: flex !important;
align-items: center !important;
gap: 8px !important;
box-sizing: border-box;
overflow: hidden;
font-family: 'Space Mono', monospace !important;
color: var(--bg-vanilla-100) !important;
.cm-completionIcon {
display: none !important;
}
&[aria-selected='true'] {
// background-color: rgba(78, 116, 248, 0.7) !important;
background: rgba(171, 189, 255, 0.04) !important;
}
}
}
}
.cm-gutters {
display: none !important;
}
.cm-scroller {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
&::-webkit-scrollbar-thumb {
display: none;
}
&::-webkit-scrollbar-track {
display: none;
}
&::-webkit-scrollbar-corner {
display: none;
}
}
.cm-line {
line-height: 36px !important;
font-family: 'Space Mono', monospace !important;
background-color: #121317 !important;
::-moz-selection {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
::selection {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
.cm-function {
color: var(--bg-robin-500) !important;
}
.chip-decorator {
background: rgba(36, 40, 52, 1) !important;
color: var(--bg-vanilla-100) !important;
border-radius: 4px;
padding: 2px 4px;
margin-right: 4px;
}
}
.cm-selectionBackground {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
}
}
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
height: 38px;
width: 38px;
border-left: transparent;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
}
}
.selected-add-ons-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
gap: 8px;
padding-bottom: 8px;
position: relative;
.add-on-content {
display: flex;
flex-direction: column;
gap: 8px;
max-width: 100%;
min-width: 100%;
min-width: 420px;
box-sizing: border-box;
position: relative;
}
}
.lightMode {
.add-ons-list {
.add-ons-tabs {
.add-on-tab-title {
color: var(--bg-ink-500) !important;
}
.tab {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
&:first-child {
border-left: 1px solid var(--bg-vanilla-300) !important;
}
}
.tab::before {
background: var(--bg-vanilla-300) !important;
}
.selected-view {
color: var(--bg-robin-500) !important;
border: 1px solid var(--bg-vanilla-300) !important;
}
.selected-view::before {
background: var(--bg-vanilla-300) !important;
}
}
.compass-button {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
}
.having-filter-container {
.having-filter-select-container {
.having-filter-select-editor {
.cm-editor {
&:focus-within {
border-color: var(--bg-vanilla-300) !important;
}
.cm-content {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
&:focus-within {
border-color: var(--bg-vanilla-300) !important;
}
}
.cm-tooltip-autocomplete {
background: var(--bg-vanilla-100) !important;
border: 1px solid var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
ul {
li {
&:hover {
background: var(--bg-vanilla-300) !important;
}
&[aria-selected='true'] {
color: var(--bg-ink-500) !important;
background: var(--bg-vanilla-300) !important;
}
}
}
}
.cm-line {
background-color: var(--bg-vanilla-100) !important;
::-moz-selection {
background: var(--bg-vanilla-100) !important;
}
::selection {
background: var(--bg-ink-100) !important;
}
.chip-decorator {
background: var(--bg-robin-100) !important;
color: var(--bg-ink-400) !important;
}
}
.cm-selectionBackground {
background: var(--bg-vanilla-100) !important;
}
}
}
.close-btn {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
}
}
}

View File

@@ -1,340 +0,0 @@
import './QueryAddOns.styles.scss';
import { Button, Radio, RadioChangeEvent } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter';
import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter';
import { ReduceToFilter } from 'container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { isEmpty } from 'lodash-es';
import { BarChart2, ScrollText, X } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import HavingFilter from './HavingFilter/HavingFilter';
interface AddOn {
icon: React.ReactNode;
label: string;
key: string;
}
const ADD_ONS_KEYS = {
GROUP_BY: 'group_by',
HAVING: 'having',
ORDER_BY: 'order_by',
LIMIT: 'limit',
LEGEND_FORMAT: 'legend_format',
};
const ADD_ONS = [
{
icon: <BarChart2 size={14} />,
label: 'Group By',
key: 'group_by',
},
{
icon: <ScrollText size={14} />,
label: 'Having',
key: 'having',
},
{
icon: <ScrollText size={14} />,
label: 'Order By',
key: 'order_by',
},
{
icon: <ScrollText size={14} />,
label: 'Limit',
key: 'limit',
},
{
icon: <ScrollText size={14} />,
label: 'Legend format',
key: 'legend_format',
},
];
const REDUCE_TO = {
icon: <ScrollText size={14} />,
label: 'Reduce to',
key: 'reduce_to',
};
function QueryAddOns({
query,
version,
isListViewPanel,
showReduceTo,
panelType,
index,
}: {
query: IBuilderQuery;
version: string;
isListViewPanel: boolean;
showReduceTo: boolean;
panelType: PANEL_TYPES | null;
index: number;
}): JSX.Element {
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
const [selectedViews, setSelectedViews] = useState<AddOn[]>([]);
const { handleChangeQueryData } = useQueryOperations({
index,
query,
entityVersion: '',
});
useEffect(() => {
if (isListViewPanel) {
setAddOns([]);
setSelectedViews([
ADD_ONS.find((addOn) => addOn.key === ADD_ONS_KEYS.ORDER_BY) as AddOn,
]);
return;
}
let filteredAddOns: AddOn[];
if (panelType === PANEL_TYPES.VALUE) {
// Filter out all add-ons except legend format
filteredAddOns = ADD_ONS.filter(
(addOn) => addOn.key === ADD_ONS_KEYS.LEGEND_FORMAT,
);
} else {
filteredAddOns = Object.values(ADD_ONS);
// Filter out group_by for metrics data source
if (query.dataSource === DataSource.METRICS) {
filteredAddOns = filteredAddOns.filter(
(addOn) => addOn.key !== ADD_ONS_KEYS.GROUP_BY,
);
}
}
// add reduce to if showReduceTo is true
if (showReduceTo) {
filteredAddOns = [...filteredAddOns, REDUCE_TO];
}
setAddOns(filteredAddOns);
// Filter selectedViews to only include add-ons present in filteredAddOns
setSelectedViews((prevSelectedViews) =>
prevSelectedViews.filter((view) =>
filteredAddOns.some((addOn) => addOn.key === view.key),
),
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [panelType, isListViewPanel, query.dataSource]);
const handleOptionClick = (e: RadioChangeEvent): void => {
if (selectedViews.find((view) => view.key === e.target.value.key)) {
setSelectedViews(
selectedViews.filter((view) => view.key !== e.target.value.key),
);
} else {
setSelectedViews([...selectedViews, e.target.value]);
}
};
const handleChangeGroupByKeys = useCallback(
(value: IBuilderQuery['groupBy']) => {
handleChangeQueryData('groupBy', value);
},
[handleChangeQueryData],
);
const handleChangeOrderByKeys = useCallback(
(value: IBuilderQuery['orderBy']) => {
handleChangeQueryData('orderBy', value);
},
[handleChangeQueryData],
);
const handleChangeReduceTo = useCallback(
(value: IBuilderQuery['reduceTo']) => {
handleChangeQueryData('reduceTo', value);
},
[handleChangeQueryData],
);
const handleRemoveView = useCallback(
(key: string): void => {
setSelectedViews(selectedViews.filter((view) => view.key !== key));
},
[selectedViews],
);
const handleChangeQueryLegend = useCallback(
(value: string) => {
handleChangeQueryData('legend', value);
},
[handleChangeQueryData],
);
const handleChangeLimit = useCallback(
(value: string) => {
handleChangeQueryData('limit', Number(value) || null);
},
[handleChangeQueryData],
);
const handleChangeHaving = useCallback(
(value: string) => {
handleChangeQueryData('havingExpression', {
expression: value,
});
},
[handleChangeQueryData],
);
return (
<div className="query-add-ons">
{selectedViews.length > 0 && (
<div className="selected-add-ons-content">
{selectedViews.find((view) => view.key === 'group_by') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Group By</div>
<div className="input">
<GroupByFilter
disabled={
query.dataSource === DataSource.METRICS &&
!query.aggregateAttribute.key
}
query={query}
onChange={handleChangeGroupByKeys}
/>
</div>
<Button
className="close-btn periscope-btn ghost"
icon={<X size={16} />}
onClick={(): void => handleRemoveView('group_by')}
/>
</div>
</div>
)}
{selectedViews.find((view) => view.key === 'having') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Having</div>
<div className="input">
<HavingFilter
onClose={(): void => {
setSelectedViews(
selectedViews.filter((view) => view.key !== 'having'),
);
}}
onChange={handleChangeHaving}
queryData={query}
/>
</div>
</div>
</div>
)}
{selectedViews.find((view) => view.key === 'limit') && (
<div className="add-on-content">
<InputWithLabel
label="Limit"
onChange={handleChangeLimit}
initialValue={query?.limit ?? undefined}
placeholder="Enter limit"
onClose={(): void => {
setSelectedViews(selectedViews.filter((view) => view.key !== 'limit'));
}}
/>
</div>
)}
{selectedViews.find((view) => view.key === 'order_by') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Order By</div>
<div className="input">
<OrderByFilter
entityVersion={version}
query={query}
onChange={handleChangeOrderByKeys}
isListViewPanel={isListViewPanel}
isNewQueryV2
/>
</div>
{!isListViewPanel && (
<Button
className="close-btn periscope-btn ghost"
icon={<X size={16} />}
onClick={(): void => handleRemoveView('order_by')}
/>
)}
</div>
</div>
)}
{selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Reduce to</div>
<div className="input">
<ReduceToFilter query={query} onChange={handleChangeReduceTo} />
</div>
<Button
className="close-btn periscope-btn ghost"
icon={<X size={16} />}
onClick={(): void => handleRemoveView('reduce_to')}
/>
</div>
</div>
)}
{selectedViews.find((view) => view.key === 'legend_format') && (
<div className="add-on-content">
<InputWithLabel
label="Legend format"
placeholder="Write legend format"
onChange={handleChangeQueryLegend}
initialValue={isEmpty(query?.legend) ? undefined : query?.legend}
onClose={(): void => {
setSelectedViews(
selectedViews.filter((view) => view.key !== 'legend_format'),
);
}}
/>
</div>
)}
</div>
)}
<div className="add-ons-list">
<Radio.Group
className="add-ons-tabs"
onChange={handleOptionClick}
value={selectedViews}
>
{addOns.map((addOn) => (
<Radio.Button
key={addOn.label}
className={
selectedViews.find((view) => view.key === addOn.key)
? 'selected-view tab'
: 'tab'
}
value={addOn}
>
<div className="add-on-tab-title">
{addOn.icon}
{addOn.label}
</div>
</Radio.Button>
))}
</Radio.Group>
</div>
</div>
);
}
export default QueryAddOns;

View File

@@ -1,300 +0,0 @@
.query-aggregation-container {
display: block;
position: relative;
.aggregation-container {
display: flex;
flex-direction: row;
gap: 8px;
align-items: flex-start;
flex-wrap: wrap;
.query-aggregation-select-container {
flex: 1;
min-width: 400px;
}
.query-aggregation-options-input {
width: 100%;
height: 36px;
line-height: 36px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
font-family: 'Space Mono', monospace !important;
&::placeholder {
color: var(--bg-vanilla-100);
opacity: 0.5;
}
}
.query-aggregation-interval {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
max-width: 360px;
.query-aggregation-interval-input-container {
.query-aggregation-interval-input {
input {
max-width: 120px;
}
}
}
}
.query-aggregation-select-container {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
min-width: 400px;
.query-aggregation-select-editor {
border-radius: 2px;
flex: 1;
min-width: 0;
.cm-content {
padding: 0;
}
.cm-editor {
border-radius: 2px;
background-color: transparent !important;
position: relative !important;
&.cm-focused {
outline: none !important;
}
&:focus-within {
border-color: var(--bg-robin-500);
}
.cm-content {
border-radius: 2px;
border: 1px solid var(--Slate-400, #1d212d);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
padding: 0px !important;
background-color: #121317 !important;
&:focus-within {
border-color: var(--bg-ink-200);
}
}
.cm-tooltip-autocomplete {
background: var(--bg-ink-300) !important;
border-radius: 2px !important;
font-size: 12px !important;
font-weight: 500 !important;
margin-top: 8px !important;
min-width: 400px !important;
position: absolute !important;
left: 0px !important;
width: 100% !important;
border-radius: 4px;
border: 1px solid var(--bg-slate-200, #1d212d);
border-top: none !important;
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
) !important;
backdrop-filter: blur(20px);
box-sizing: border-box;
font-family: 'Space Mono', monospace !important;
ul {
width: 100% !important;
max-width: 100% !important;
font-family: 'Space Mono', monospace !important;
min-height: 200px !important;
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
li {
width: 100% !important;
max-width: 100% !important;
line-height: 36px !important;
height: 36px !important;
padding: 4px 8px !important;
display: flex !important;
align-items: center !important;
gap: 8px !important;
box-sizing: border-box;
overflow: hidden;
font-family: 'Space Mono', monospace !important;
.cm-completionIcon {
display: none !important;
}
&[aria-selected='true'] {
// background-color: rgba(78, 116, 248, 0.7) !important;
background: rgba(171, 189, 255, 0.04) !important;
}
}
}
}
.cm-gutters {
display: none !important;
}
.cm-line {
line-height: 36px !important;
font-family: 'Space Mono', monospace !important;
background-color: #121317 !important;
::-moz-selection {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
::selection {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
.cm-function {
color: var(--bg-robin-500) !important;
}
.chip-decorator {
background: rgba(36, 40, 52, 1) !important;
color: var(--bg-vanilla-100) !important;
border-radius: 4px;
padding: 2px 4px;
margin-right: 4px;
}
}
.cm-selectionBackground {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
}
}
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
height: 38px;
width: 38px;
border-left: transparent;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
}
}
}
.lightMode {
.query-aggregation-container {
.aggregation-container {
.query-aggregation-options-input {
border-color: var(--bg-vanilla-300) !important;
&::placeholder {
color: var(--bg-ink-400) !important;
opacity: 0.5 !important;
}
}
.query-aggregation-select-container {
.query-aggregation-select-editor {
.cm-editor {
.cm-content {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1) !important;
&:focus-within {
border-color: var(--bg-vanilla-300) !important;
}
}
.cm-tooltip-autocomplete {
background: var(--bg-vanilla-100) !important;
border: 1px solid var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
ul {
li {
&:hover {
background-color: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
}
&[aria-selected='true'] {
background: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
}
}
}
}
.cm-line {
background-color: var(--bg-vanilla-100) !important;
::-moz-selection {
background: var(--bg-vanilla-100) !important;
opacity: 0.5 !important;
}
::selection {
background: var(--bg-vanilla-100) !important;
opacity: 0.5 !important;
}
.cm-function {
color: var(--bg-robin-500) !important;
}
.chip-decorator {
background: var(--bg-robin-500) !important;
color: var(--bg-ink-400) !important;
}
}
// .cm-selectionBackground {
// background: var(--bg-vanilla-100) !important;
// opacity: 0.5 !important;
// }
}
}
.close-btn {
border-color: var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
}
}
}
}

View File

@@ -1,75 +0,0 @@
import './QueryAggregation.styles.scss';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import QueryAggregationSelect from './QueryAggregationSelect';
function QueryAggregationOptions({
dataSource,
panelType,
onAggregationIntervalChange,
onChange,
queryData,
}: {
dataSource: DataSource;
panelType?: string;
onAggregationIntervalChange: (value: number) => void;
onChange?: (value: string) => void;
queryData: IBuilderQuery;
}): JSX.Element {
const showAggregationInterval = useMemo(() => {
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
if (panelType === PANEL_TYPES.VALUE) {
return false;
}
if (dataSource === DataSource.TRACES || dataSource === DataSource.LOGS) {
return !(panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.PIE);
}
return true;
}, [dataSource, panelType]);
const handleAggregationIntervalChange = (value: string): void => {
onAggregationIntervalChange(Number(value));
};
return (
<div className="query-aggregation-container">
<div className="aggregation-container">
<QueryAggregationSelect onChange={onChange} queryData={queryData} />
{showAggregationInterval && (
<div className="query-aggregation-interval">
<div className="query-aggregation-interval-label">every</div>
<div className="query-aggregation-interval-input-container">
<InputWithLabel
initialValue={
queryData.stepInterval ? queryData.stepInterval : undefined
}
className="query-aggregation-interval-input"
label="Seconds"
placeholder="Auto"
type="number"
onChange={handleAggregationIntervalChange}
labelAfter
onClose={(): void => {}}
/>
</div>
</div>
)}
</div>
</div>
);
}
QueryAggregationOptions.defaultProps = {
panelType: null,
onChange: undefined,
};
export default QueryAggregationOptions;

View File

@@ -1,530 +0,0 @@
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable no-cond-assign */
/* eslint-disable no-restricted-syntax */
/* eslint-disable class-methods-use-this */
/* eslint-disable react/no-this-in-sfc */
/* eslint-disable sonarjs/cognitive-complexity */
import './QueryAggregation.styles.scss';
import {
autocompletion,
closeCompletion,
Completion,
CompletionContext,
completionKeymap,
CompletionResult,
startCompletion,
} from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import { RangeSetBuilder } from '@codemirror/state';
import { copilot } from '@uiw/codemirror-theme-copilot';
import CodeMirror, {
Decoration,
EditorView,
keymap,
ViewPlugin,
ViewUpdate,
} from '@uiw/react-codemirror';
import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { TracesAggregatorOperator } from 'types/common/queryBuilder';
import { useQueryBuilderV2Context } from '../../QueryBuilderV2Context';
const chipDecoration = Decoration.mark({
class: 'chip-decorator',
});
const operatorArgMeta: Record<
string,
{ acceptsArgs: boolean; multiple: boolean }
> = {
[TracesAggregatorOperator.NOOP]: { acceptsArgs: false, multiple: false },
[TracesAggregatorOperator.COUNT]: { acceptsArgs: false, multiple: false },
[TracesAggregatorOperator.COUNT_DISTINCT]: {
acceptsArgs: true,
multiple: true,
},
[TracesAggregatorOperator.SUM]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.AVG]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.MAX]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.MIN]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P05]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P10]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P20]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P25]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P50]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P75]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P90]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P95]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P99]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.RATE]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.RATE_SUM]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.RATE_AVG]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.RATE_MIN]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.RATE_MAX]: { acceptsArgs: true, multiple: false },
};
function getFunctionContextAtCursor(
text: string,
cursorPos: number,
): string | null {
// Find the nearest function name to the left of the nearest unmatched '('
let openParenIndex = -1;
let funcName: string | null = null;
let parenStack = 0;
for (let i = cursorPos - 1; i >= 0; i--) {
if (text[i] === ')') parenStack++;
else if (text[i] === '(') {
if (parenStack === 0) {
openParenIndex = i;
const before = text.slice(0, i);
const match = before.match(/(\w+)\s*$/);
if (match) funcName = match[1].toLowerCase();
break;
}
parenStack--;
}
}
if (openParenIndex === -1 || !funcName) return null;
// Scan forwards to find the matching closing parenthesis
let closeParenIndex = -1;
let depth = 1;
for (let j = openParenIndex + 1; j < text.length; j++) {
if (text[j] === '(') depth++;
else if (text[j] === ')') depth--;
if (depth === 0) {
closeParenIndex = j;
break;
}
}
if (
cursorPos > openParenIndex &&
(closeParenIndex === -1 || cursorPos <= closeParenIndex)
) {
return funcName;
}
return null;
}
// Custom extension to stop events from propagating to global shortcuts
const stopEventsExtension = EditorView.domEventHandlers({
keydown: (event) => {
// Stop all keyboard events from propagating to global shortcuts
event.stopPropagation();
event.stopImmediatePropagation();
return false; // Important for CM to know you handled it
},
input: (event) => {
event.stopPropagation();
return false;
},
focus: (event) => {
// Ensure focus events don't interfere with global shortcuts
event.stopPropagation();
return false;
},
blur: (event) => {
// Ensure blur events don't interfere with global shortcuts
event.stopPropagation();
return false;
},
});
// eslint-disable-next-line react/no-this-in-sfc
function QueryAggregationSelect({
onChange,
queryData,
}: {
onChange?: (value: string) => void;
queryData: IBuilderQuery;
}): JSX.Element {
const { setAggregationOptions } = useQueryBuilderV2Context();
const [input, setInput] = useState(
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
);
useEffect(() => {
setInput(
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
);
}, [queryData?.aggregations]);
const [cursorPos, setCursorPos] = useState(0);
const [functionArgPairs, setFunctionArgPairs] = useState<
{ func: string; arg: string }[]
>([]);
const editorRef = useRef<EditorView | null>(null);
const [isFocused, setIsFocused] = useState(false);
// Helper function to safely start completion
const safeStartCompletion = useCallback((): void => {
requestAnimationFrame(() => {
if (editorRef.current) {
startCompletion(editorRef.current);
}
});
}, []);
// Update cursor position on every editor update
const handleUpdate = (update: { view: EditorView }): void => {
const pos = update.view.state.selection.main.from;
setCursorPos(pos);
};
// Effect to handle focus state and trigger suggestions
useEffect(() => {
if (isFocused) {
safeStartCompletion();
}
}, [isFocused, safeStartCompletion]);
// Extract all valid function-argument pairs from the input
useEffect(() => {
const pairs: { func: string; arg: string }[] = [];
const regex = /([a-zA-Z_][\w]*)\s*\(([^)]*)\)/g;
let match;
while ((match = regex.exec(input)) !== null) {
const func = match[1].toLowerCase();
const args = match[2]
.split(',')
.map((arg) => arg.trim())
.filter((arg) => arg.length > 0);
if (args.length === 0) {
// For functions with no arguments, add a pair with empty string as arg
pairs.push({ func, arg: '' });
} else {
args.forEach((arg) => {
pairs.push({ func, arg });
});
}
}
setFunctionArgPairs(pairs);
setAggregationOptions(pairs);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [input]);
// Find function context for fetching suggestions
const functionContextForFetch = getFunctionContextAtCursor(input, cursorPos);
const { data: aggregateAttributeData, isLoading: isLoadingFields } = useQuery(
[
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
functionContextForFetch,
queryData.dataSource,
],
() =>
getAggregateAttribute({
searchText: '',
aggregateOperator: functionContextForFetch as string,
dataSource: queryData.dataSource,
}),
{
enabled:
!!functionContextForFetch &&
!!operatorArgMeta[functionContextForFetch]?.acceptsArgs,
},
);
// Get valid function names (lowercase)
const validFunctions = useMemo(
() => tracesAggregateOperatorOptions.map((op) => op.value.toLowerCase()),
[],
);
// Memoized chipPlugin that highlights valid function calls like count(), max(arg), min(arg)
const chipPlugin = useMemo(
() =>
ViewPlugin.fromClass(
class {
decorations: import('@codemirror/view').DecorationSet;
constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}
update(update: ViewUpdate): void {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
buildDecorations(
view: EditorView,
): import('@codemirror/view').DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
for (const { from, to } of view.visibleRanges) {
const text = view.state.doc.sliceString(from, to);
const regex = /\b([a-zA-Z_][\w]*)\s*\(([^)]*)\)/g;
let match;
while ((match = regex.exec(text)) !== null) {
const func = match[1].toLowerCase();
if (validFunctions.includes(func)) {
const start = from + match.index;
const end = start + match[0].length;
builder.add(start, end, chipDecoration);
}
}
}
return builder.finish();
}
},
{
decorations: (v: any): import('@codemirror/view').DecorationSet =>
v.decorations,
},
),
[validFunctions],
) as any;
const operatorCompletions: Completion[] = tracesAggregateOperatorOptions.map(
(op) => ({
label: op.value,
type: 'function',
info: op.label,
apply: (
view: EditorView,
completion: Completion,
from: number,
to: number,
): void => {
const acceptsArgs = operatorArgMeta[op.value]?.acceptsArgs;
let insertText: string;
let cursorPos: number;
if (!acceptsArgs) {
insertText = `${op.value}() `;
cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
} else {
insertText = `${op.value}(`;
cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
}
view.dispatch({
changes: { from, to, insert: insertText },
selection: { anchor: cursorPos },
});
// Trigger suggestions after a small delay
setTimeout(() => {
safeStartCompletion();
}, 50);
},
}),
);
// Memoize field suggestions from API (no filtering here)
const fieldSuggestions = useMemo(
() =>
aggregateAttributeData?.payload?.attributeKeys?.map(
(attributeKey: BaseAutocompleteData) => ({
label: attributeKey.key,
type: 'variable',
info: attributeKey.dataType,
apply: (
view: EditorView,
completion: Completion,
from: number,
to: number,
): void => {
const text = view.state.sliceDoc(0, from);
const funcName = getFunctionContextAtCursor(text, from);
const multiple = funcName ? operatorArgMeta[funcName]?.multiple : false;
// Insert the selected key followed by either a comma or closing parenthesis
const insertText = multiple
? `${completion.label},`
: `${completion.label}) `;
const cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
view.dispatch({
changes: { from, to, insert: insertText },
selection: { anchor: cursorPos },
});
// Trigger next suggestions after a small delay
setTimeout(() => {
safeStartCompletion();
}, 50);
},
}),
) || [],
[aggregateAttributeData, safeStartCompletion],
);
const aggregatorAutocomplete = useMemo(
() =>
autocompletion({
override: [
(context: CompletionContext): CompletionResult | null => {
const text = context.state.sliceDoc(0, context.state.doc.length);
const cursorPos = context.pos;
const funcName = getFunctionContextAtCursor(text, cursorPos);
// Do not show suggestions if inside count()
if (
funcName === TracesAggregatorOperator.COUNT &&
cursorPos > 0 &&
text[cursorPos - 1] !== ')'
) {
return null;
}
// If inside a function that accepts args, show field suggestions
if (funcName && operatorArgMeta[funcName]?.acceptsArgs) {
if (isLoadingFields) {
return {
from: cursorPos,
options: [
{
label: 'Loading suggestions...',
type: 'text',
apply: (): void => {},
},
],
};
}
const doc = context.state.sliceDoc(0, cursorPos);
const lastOpenParen = doc.lastIndexOf('(');
const lastComma = doc.lastIndexOf(',', cursorPos - 1);
const startOfArg =
lastComma > lastOpenParen ? lastComma + 1 : lastOpenParen + 1;
const inputText = doc.slice(startOfArg, cursorPos).trim();
// Parse arguments already present in the function call (before the cursor)
const usedArgs = new Set<string>();
if (lastOpenParen !== -1) {
const argsString = doc.slice(lastOpenParen + 1, cursorPos);
argsString.split(',').forEach((arg) => {
const trimmed = arg.trim();
if (trimmed) usedArgs.add(trimmed);
});
}
// Exclude arguments already paired with this function elsewhere in the input
const globalUsedArgs = new Set(
functionArgPairs
.filter((pair) => pair.func === funcName)
.map((pair) => pair.arg),
);
const availableSuggestions = fieldSuggestions.filter(
(suggestion) =>
!usedArgs.has(suggestion.label) &&
!globalUsedArgs.has(suggestion.label),
);
const filteredSuggestions =
inputText === ''
? availableSuggestions
: availableSuggestions.filter((suggestion) =>
suggestion.label.toLowerCase().includes(inputText.toLowerCase()),
);
return {
from: startOfArg,
options: filteredSuggestions,
};
}
// Show operator suggestions if no function context or not accepting args
if (!funcName || !operatorArgMeta[funcName]?.acceptsArgs) {
// Check if 'count(' is present in the current input (case-insensitive)
const hasCount = text.toLowerCase().includes('count(');
const availableOperators = hasCount
? operatorCompletions.filter((op) => op.label.toLowerCase() !== 'count')
: operatorCompletions;
// Get the word before cursor if any
const word = context.matchBefore(/[\w\d_]+/);
// Show suggestions if:
// 1. There's a word match
// 2. The input is empty (cursor at start)
// 3. The user explicitly triggered completion
if (word || cursorPos === 0 || context.explicit) {
return {
from: word ? word.from : cursorPos,
options: availableOperators,
};
}
}
return null;
},
],
defaultKeymap: true,
closeOnBlur: true,
maxRenderedOptions: 50,
activateOnTyping: true,
}),
[operatorCompletions, isLoadingFields, fieldSuggestions, functionArgPairs],
);
return (
<div className="query-aggregation-select-container">
<CodeMirror
value={input}
onChange={(value): void => {
setInput(value);
onChange?.(value);
}}
className="query-aggregation-select-editor"
theme={copilot}
extensions={[
chipPlugin,
aggregatorAutocomplete,
javascript({ jsx: false, typescript: false }),
EditorView.lineWrapping,
stopEventsExtension,
keymap.of([
...completionKeymap,
{
key: 'Escape',
run: closeCompletion,
},
]),
]}
placeholder="Type aggregator functions like sum(), count_distinct(...), etc."
basicSetup={{
lineNumbers: false,
autocompletion: true,
completionKeymap: true,
}}
onUpdate={handleUpdate}
onCreateEditor={(view: EditorView): void => {
editorRef.current = view;
}}
onFocus={(): void => {
setIsFocused(true);
safeStartCompletion();
}}
onBlur={(): void => {
setIsFocused(false);
if (editorRef.current) {
closeCompletion(editorRef.current);
}
}}
/>
</div>
);
}
QueryAggregationSelect.defaultProps = {
onChange: undefined,
};
export default QueryAggregationSelect;

View File

@@ -1,35 +0,0 @@
import { Button } from 'antd';
import { Plus, Sigma } from 'lucide-react';
export default function QueryFooter({
addNewBuilderQuery,
addNewFormula,
}: {
addNewBuilderQuery: () => void;
addNewFormula: () => void;
}): JSX.Element {
return (
<div className="qb-footer">
<div className="qb-footer-container">
<div className="qb-add-new-query">
<Button
className="add-new-query-button periscope-btn secondary"
type="text"
icon={<Plus size={16} />}
onClick={addNewBuilderQuery}
/>
</div>
<div className="qb-add-formula">
<Button
className="add-formula-button periscope-btn secondary"
icon={<Sigma size={16} />}
onClick={addNewFormula}
>
Add Formula
</Button>
</div>
</div>
</div>
);
}

View File

@@ -1,713 +0,0 @@
.code-mirror-where-clause {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', sans-serif;
.query-where-clause-editor-container {
display: flex;
flex-direction: row;
.query-where-clause-editor {
flex: 1;
min-width: 400px;
}
.query-status-container {
width: 32px;
background-color: #121317 !important;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--bg-slate-200);
border-radius: 2px;
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
border-left: none !important;
&.hasErrors {
border-color: var(--bg-cherry-500);
}
}
}
.query-where-clause-editor {
&.hasErrors {
.cm-editor {
.cm-content {
border-color: var(--bg-cherry-500);
border-top-right-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
}
}
}
.cm-editor {
border-radius: 2px;
overflow: hidden;
background-color: transparent !important;
&:focus-within {
border-color: var(--bg-robin-500);
}
.cm-content {
border-radius: 2px;
border: 1px solid var(--Slate-400, #1d212d);
padding: 0px !important;
background-color: #121317 !important;
&:focus-within {
border-color: var(--bg-ink-200);
}
}
&.cm-focused {
outline: 1px solid var(--bg-slate-200);
}
.cm-tooltip-autocomplete {
background: var(--bg-ink-300) !important;
border-radius: 2px !important;
font-size: 12px !important;
font-weight: 500 !important;
margin-top: -2px !important;
min-width: 400px !important;
position: relative !important;
top: 0px !important;
left: 0px !important;
border-radius: 4px;
border: 0px;
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
) !important;
backdrop-filter: blur(20px);
box-sizing: border-box;
font-family: 'Space Mono', monospace !important;
ul {
width: 100% !important;
max-width: 100% !important;
font-family: 'Space Mono', monospace !important;
min-height: 200px !important;
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
li {
width: 100% !important;
max-width: 100% !important;
line-height: 36px !important;
height: 36px !important;
padding: 4px 8px !important;
display: flex !important;
align-items: center !important;
gap: 8px !important;
box-sizing: border-box;
overflow: hidden;
font-family: 'Space Mono', monospace !important;
&:hover {
background: var(--bg-ink-100) !important;
}
.cm-completionIcon {
display: none !important;
}
&[aria-selected='true'] {
// background-color: rgba(78, 116, 248, 0.7) !important;
background: rgba(171, 189, 255, 0.04) !important;
}
}
}
}
.cm-gutters {
display: none !important;
}
.cm-line {
line-height: 34px !important;
font-family: 'Space Mono', monospace !important;
background-color: #121317 !important;
::-moz-selection {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
::selection {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
}
.cm-selectionBackground {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
}
.cursor-position {
font-size: 12px;
color: var(--bg-ink-200);
padding: 6px;
background-color: var(--bg-vanilla-200);
border-radius: 4px;
display: inline-flex;
align-items: center;
margin-bottom: 8px;
margin-top: 8px;
}
.query-validation {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 8px;
margin-top: 16px;
.valid,
.invalid {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
font-size: 12px;
}
.valid {
background-color: rgba(39, 174, 96, 0.1);
color: #27ae60;
}
.invalid {
background-color: rgba(235, 87, 87, 0.1);
color: #eb5757;
}
.query-validation-status {
display: flex;
align-items: center;
gap: 8px;
}
.query-validation-errors {
display: flex;
flex-direction: column;
gap: 8px;
.query-validation-error {
display: flex;
flex-direction: row;
gap: 16px;
font-size: 12px;
font-family: 'Space Mono', monospace !important;
color: var(--bg-cherry-500);
}
}
}
.query-context {
padding: 12px;
background-color: var(--bg-ink-400);
border-radius: 4px;
border-left: 3px solid var(--bg-robin-500);
color: var(--bg-ink-300) !important;
.ant-card-head {
color: var(--bg-vanilla-300) !important;
}
.context-details {
display: flex;
flex-wrap: wrap;
gap: 12px;
p {
margin: 0;
font-size: 13px;
strong {
color: var(--bg-vanilla-300);
margin-right: 4px;
}
}
}
}
.code-mirror-card {
.ant-card-body {
padding: 8px;
}
}
.query-text-preview-title {
font-size: 13px;
color: var(--bg-vanilla-100);
background-color: var(--bg-robin-500);
padding: 2px 6px;
border-radius: 2px;
margin-right: 4px;
}
.query-text-preview {
font-family: 'Space Mono', monospace;
font-size: 13px;
color: var(--bg-vanilla-200);
padding: 2px 6px;
font-style: italic;
}
.query-examples-card {
background-color: var(--bg-ink-400);
border: 1px solid var(--bg-slate-200);
.ant-card-body {
padding: 0;
}
.query-examples {
.ant-collapse-header {
padding: 8px 16px !important;
color: var(--bg-vanilla-300) !important;
font-weight: 500;
}
.ant-collapse-content {
background-color: transparent !important;
}
.query-examples-list {
display: flex;
flex-direction: row;
gap: 8px;
flex-wrap: wrap;
}
.query-example-tag {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 12px;
background-color: var(--bg-ink-400);
border: 1px solid var(--bg-slate-200);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
&:hover {
background-color: var(--bg-ink-300);
border-color: var(--bg-robin-500);
}
&:focus-visible {
outline: 2px solid var(--bg-robin-500);
outline-offset: 2px;
}
.query-example-content {
display: flex;
align-items: center;
gap: 8px;
}
.query-example-label {
font-weight: 500;
color: var(--bg-vanilla-300);
font-size: 13px;
}
.query-example-query {
font-family: 'Space Mono', monospace;
font-size: 12px;
color: var(--bg-vanilla-200);
background-color: var(--bg-ink-300);
padding: 2px 6px;
border-radius: 2px;
}
.query-example-description {
font-size: 12px;
color: var(--bg-vanilla-200);
opacity: 0.8;
}
}
.query-example-content {
display: inline-flex;
cursor: pointer;
}
}
}
// Context indicator styles
.context-indicator {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: 8px 12px;
margin-bottom: 8px;
border-radius: 4px;
font-size: 13px;
background-color: #f5f5f5;
border-left: 4px solid #1890ff;
display: none;
.triplet-info {
margin-left: 16px;
display: inline-flex;
align-items: center;
gap: 4px;
}
.query-pair-info {
display: inline-flex;
align-items: center;
gap: 4px;
border-left: 1px solid rgba(0, 0, 0, 0.1);
padding-left: 8px;
background-color: rgba(0, 0, 0, 0.03);
padding: 4px 8px;
border-radius: 4px;
}
// Color variations based on context
&.context-indicator-key {
border-left-color: #1890ff; // blue
background-color: rgba(24, 144, 255, 0.1);
}
&.context-indicator-operator {
border-left-color: #722ed1; // purple
background-color: rgba(114, 46, 209, 0.1);
}
&.context-indicator-value {
border-left-color: #52c41a; // green
background-color: rgba(82, 196, 26, 0.1);
}
&.context-indicator-conjunction {
border-left-color: #fa8c16; // orange
background-color: rgba(250, 140, 22, 0.1);
}
&.context-indicator-function {
border-left-color: #13c2c2; // cyan
background-color: rgba(19, 194, 194, 0.1);
}
&.context-indicator-parenthesis {
border-left-color: #eb2f96; // magenta
background-color: rgba(235, 47, 150, 0.1);
}
}
}
.query-status-popover {
.ant-popover-arrow {
display: none !important;
}
.ant-popover-content {
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
margin-top: -6px !important;
}
}
// /* Dark mode support */
// :global(.darkMode) {
// .code-mirror-where-clause {
// .cm-editor {
// border-color: var(--bg-slate-500);
// background-color: var(--bg-ink-400);
// }
// .cursor-position {
// background-color: var(--bg-ink-400);
// color: var(--bg-vanilla-100);
// }
// .query-context {
// background-color: var(--bg-ink-400);
// color: var(--bg-vanilla-100);
// h3 {
// color: var(--bg-vanilla-100);
// }
// .context-details {
// p {
// strong {
// color: var(--bg-vanilla-200);
// }
// }
// }
// }
// .query-examples-card {
// background-color: var(--bg-ink-400);
// border-color: var(--bg-slate-500);
// .ant-collapse-header {
// color: var(--bg-vanilla-100) !important;
// }
// .query-example-tag {
// background-color: var(--bg-ink-400);
// border-color: var(--bg-slate-500);
// &:hover {
// background-color: var(--bg-ink-300);
// border-color: var(--bg-robin-500);
// }
// .query-example-label {
// color: var(--bg-vanilla-100);
// }
// .query-example-query {
// color: var(--bg-vanilla-100);
// background-color: var(--bg-ink-300);
// }
// .query-example-description {
// color: var(--bg-vanilla-100);
// }
// }
// }
// .context-indicator {
// background-color: var(--bg-ink-300);
// color: var(--bg-vanilla-100);
// .query-pair-info {
// border-left: 1px solid rgba(255, 255, 255, 0.1);
// background-color: rgba(255, 255, 255, 0.05);
// }
// }
// }
// }
.lightMode {
.code-mirror-where-clause {
.query-where-clause-editor-container {
.query-status-container {
background-color: var(--bg-vanilla-100) !important;
border: 1px solid var(--bg-vanilla-300);
&.hasErrors {
border-color: var(--bg-cherry-500);
}
}
}
.query-where-clause-editor {
&.hasErrors {
.cm-editor {
.cm-content {
border-color: var(--bg-cherry-500);
border-top-right-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
}
}
}
.cm-editor {
&:focus-within {
border-color: var(--bg-robin-500);
}
&.cm-focused {
outline: 1px solid var(--bg-vanilla-300);
}
.cm-content {
border-radius: 2px;
border: 1px solid var(--bg-vanilla-300);
padding: 0px !important;
background-color: var(--bg-vanilla-100) !important;
&:focus-within {
border-color: var(--bg-vanilla-200);
}
}
.cm-tooltip-autocomplete {
background: var(--bg-vanilla-100) !important;
border: 0px;
backdrop-filter: blur(20px);
ul {
li {
background-color: var(--bg-vanilla-100) !important;
color: var(--bg-ink-300) !important;
&:hover {
background-color: var(--bg-vanilla-200) !important;
}
}
}
}
.cm-line {
background-color: var(--bg-vanilla-100) !important;
::-moz-selection {
background: var(--bg-vanilla-100) !important;
}
::selection {
background: var(--bg-vanilla-100) !important;
}
}
.cm-selectionBackground {
background: var(--bg-vanilla-100) !important;
}
}
.cursor-position {
color: var(--bg-vanilla-200);
background-color: var(--bg-vanilla-100);
}
.query-context {
background-color: var(--bg-vanilla-100);
border-left: 3px solid var(--bg-vanilla-300);
color: var(--bg-vanilla-300) !important;
.ant-card-head {
color: var(--bg-ink-300) !important;
}
.context-details {
p {
strong {
color: var(--bg-ink-300);
}
}
}
}
.query-examples-card {
background-color: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
.query-examples {
.ant-collapse-header {
color: var(--bg-ink-300) !important;
}
.query-example-tag {
background-color: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
&:hover {
background-color: var(--bg-vanilla-200);
border-color: var(--bg-vanilla-300);
}
.query-example-label {
color: var(--bg-ink-300);
}
.query-example-query {
color: var(--bg-ink-300);
background-color: var(--bg-vanilla-100);
}
.query-example-description {
color: var(--bg-ink-300);
}
}
}
}
.context-indicator {
background-color: var(--bg-vanilla-100);
border-left: 4px solid var(--bg-vanilla-300);
display: none;
.query-pair-info {
border-left: 1px solid rgba(255, 255, 255, 0.1);
background-color: rgba(255, 255, 255, 0.03);
}
// Color variations based on context
&.context-indicator-key {
border-left-color: #1890ff; // blue
background-color: rgba(24, 144, 255, 0.1);
}
&.context-indicator-operator {
border-left-color: #722ed1; // purple
background-color: rgba(114, 46, 209, 0.1);
}
&.context-indicator-value {
border-left-color: #52c41a; // green
background-color: rgba(82, 196, 26, 0.1);
}
&.context-indicator-conjunction {
border-left-color: #fa8c16; // orange
background-color: rgba(250, 140, 22, 0.1);
}
&.context-indicator-function {
border-left-color: #13c2c2; // cyan
background-color: rgba(19, 194, 194, 0.1);
}
&.context-indicator-parenthesis {
border-left-color: #eb2f96; // magenta
background-color: rgba(235, 47, 150, 0.1);
}
}
}
.query-status-popover {
.ant-popover-content {
background: var(--bg-vanilla-100);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
}

View File

@@ -1,79 +0,0 @@
export const queryExamples = [
{
label: 'Basic Query',
query: "status = 'error'",
description: 'Find all errors',
},
{
label: 'Multiple Conditions',
query: "status = 'error' AND service = 'frontend'",
description: 'Find errors from frontend service',
},
{
label: 'IN Operator',
query: "status IN ['error', 'warning']",
description: 'Find items with specific statuses',
},
{
label: 'Function Usage',
query: "HAS(service, 'frontend')",
description: 'Use HAS function',
},
{
label: 'Numeric Comparison',
query: 'duration > 1000',
description: 'Find items with duration greater than 1000ms',
},
{
label: 'Range Query',
query: 'duration BETWEEN 100 AND 1000',
description: 'Find items with duration between 100ms and 1000ms',
},
{
label: 'Pattern Matching',
query: "service LIKE 'front%'",
description: 'Find services starting with "front"',
},
{
label: 'Complex Conditions',
query: "(status = 'error' OR status = 'warning') AND service = 'frontend'",
description: 'Find errors or warnings from frontend service',
},
{
label: 'Multiple Functions',
query: "HAS(service, 'frontend') AND HAS(status, 'error')",
description: 'Use multiple HAS functions',
},
{
label: 'NOT Operator',
query: "NOT status = 'success'",
description: 'Find items that are not successful',
},
{
label: 'Array Contains',
query: "tags CONTAINS 'production'",
description: 'Find items with production tag',
},
{
label: 'Regex Pattern',
query: "service REGEXP '^prod-.*'",
description: 'Find services matching regex pattern',
},
{
label: 'Null Check',
query: 'error IS NULL',
description: 'Find items without errors',
},
{
label: 'Multiple Attributes',
query:
"service = 'frontend' AND environment = 'production' AND status = 'error'",
description: 'Find production frontend errors',
},
{
label: 'Nested Conditions',
query:
"(service = 'frontend' OR service = 'backend') AND (status = 'error' OR status = 'warning')",
description: 'Find errors or warnings from frontend or backend',
},
];

View File

@@ -1,234 +0,0 @@
import { Dropdown } from 'antd';
import cx from 'classnames';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import QBEntityOptions from 'container/QueryBuilder/components/QBEntityOptions/QBEntityOptions';
import { QueryProps } from 'container/QueryBuilder/components/Query/Query.interfaces';
import SpanScopeSelector from 'container/QueryBuilder/filters/QueryBuilderSearchV2/SpanScopeSelector';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Copy, Ellipsis, Trash } from 'lucide-react';
import { memo, useCallback, useMemo, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { HandleChangeQueryDataV5 } from 'types/common/operations.types';
import { DataSource } from 'types/common/queryBuilder';
import MetricsAggregateSection from './MerticsAggregateSection/MetricsAggregateSection';
import { MetricsSelect } from './MetricsSelect/MetricsSelect';
import QueryAddOns from './QueryAddOns/QueryAddOns';
import QueryAggregation from './QueryAggregation/QueryAggregation';
import QuerySearch from './QuerySearch/QuerySearch';
export const QueryV2 = memo(function QueryV2({
ref,
index,
queryVariant,
query,
filterConfigs,
isListViewPanel = false,
version,
showOnlyWhereClause = false,
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
const { cloneQuery, panelType } = useQueryBuilder();
const showFunctions = query?.functions?.length > 0;
const { dataSource } = query;
const [isCollapsed, setIsCollapsed] = useState(false);
const {
handleChangeQueryData,
handleDeleteQuery,
handleQueryFunctionsUpdates,
handleChangeDataSource,
} = useQueryOperations({
index,
query,
filterConfigs,
isListViewPanel,
entityVersion: version,
});
const handleToggleDisableQuery = useCallback(() => {
handleChangeQueryData('disabled', !query.disabled);
}, [handleChangeQueryData, query]);
const handleToggleCollapsQuery = (): void => {
setIsCollapsed(!isCollapsed);
};
const handleCloneEntity = (): void => {
cloneQuery('query', query);
};
const showReduceTo = useMemo(
() =>
dataSource === DataSource.METRICS &&
(panelType === PANEL_TYPES.TABLE ||
panelType === PANEL_TYPES.PIE ||
panelType === PANEL_TYPES.VALUE),
[dataSource, panelType],
);
const showSpanScopeSelector = useMemo(() => dataSource === DataSource.TRACES, [
dataSource,
]);
const handleChangeAggregateEvery = useCallback(
(value: IBuilderQuery['stepInterval']) => {
handleChangeQueryData('stepInterval', value);
},
[handleChangeQueryData],
);
const handleSearchChange = useCallback(
(value: string) => {
(handleChangeQueryData as HandleChangeQueryDataV5)('filter', {
expression: value,
});
},
[handleChangeQueryData],
);
const handleChangeAggregation = useCallback(
(value: string) => {
(handleChangeQueryData as HandleChangeQueryDataV5)('aggregations', [
{
expression: value,
},
]);
},
[handleChangeQueryData],
);
return (
<div
className={cx('query-v2', { 'where-clause-view': showOnlyWhereClause })}
ref={ref}
>
<div className="qb-content-section">
{!showOnlyWhereClause && (
<div className="qb-header-container">
<div className="query-actions-container">
<div className="query-actions-left-container">
<QBEntityOptions
isMetricsDataSource={dataSource === DataSource.METRICS}
showFunctions={
(version && version === ENTITY_VERSION_V4) ||
query.dataSource === DataSource.LOGS ||
showFunctions ||
false
}
isCollapsed={isCollapsed}
entityType="query"
entityData={query}
onToggleVisibility={handleToggleDisableQuery}
onDelete={handleDeleteQuery}
onCloneQuery={cloneQuery}
onCollapseEntity={handleToggleCollapsQuery}
query={query}
onQueryFunctionsUpdates={handleQueryFunctionsUpdates}
showDeleteButton={false}
showCloneOption={false}
isListViewPanel={isListViewPanel}
index={index}
queryVariant={queryVariant}
onChangeDataSource={handleChangeDataSource}
/>
</div>
{!isListViewPanel && (
<Dropdown
className="query-actions-dropdown"
menu={{
items: [
{
label: 'Clone',
key: 'clone-query',
icon: <Copy size={14} />,
onClick: handleCloneEntity,
},
{
label: 'Delete',
key: 'delete-query',
icon: <Trash size={14} />,
onClick: handleDeleteQuery,
},
],
}}
placement="bottomRight"
>
<Ellipsis size={16} />
</Dropdown>
)}
</div>
</div>
)}
{!isCollapsed && (
<div className="qb-elements-container">
<div className="qb-search-container">
{dataSource === DataSource.METRICS && (
<div className="metrics-select-container">
<MetricsSelect query={query} index={index} version="v4" />
</div>
)}
<div className="qb-search-filter-container">
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
/>
</div>
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
</div>
)}
</div>
</div>
{!showOnlyWhereClause &&
!isListViewPanel &&
dataSource !== DataSource.METRICS && (
<QueryAggregation
dataSource={dataSource}
key={`query-search-${query.queryName}-${query.dataSource}`}
panelType={panelType || undefined}
onAggregationIntervalChange={handleChangeAggregateEvery}
onChange={handleChangeAggregation}
queryData={query}
/>
)}
{!showOnlyWhereClause && dataSource === DataSource.METRICS && (
<MetricsAggregateSection
panelType={panelType}
query={query}
index={index}
key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`}
version="v4"
/>
)}
{!showOnlyWhereClause && (
<QueryAddOns
index={index}
query={query}
version="v3"
isListViewPanel={isListViewPanel}
showReduceTo={showReduceTo}
panelType={panelType}
/>
)}
</div>
)}
</div>
</div>
);
});

View File

@@ -1,185 +0,0 @@
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Having, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import {
LogAggregation,
MetricAggregation,
TraceAggregation,
} from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
/**
* Check if an operator requires array values (like IN, NOT IN)
* @param operator - The operator to check
* @returns True if the operator requires array values
*/
const isArrayOperator = (operator: string): boolean => {
const arrayOperators = ['in', 'nin', 'IN', 'NOT IN'];
return arrayOperators.includes(operator);
};
/**
* Format a value for the expression string
* @param value - The value to format
* @param operator - The operator being used (to determine if array is needed)
* @returns Formatted value string
*/
const formatValueForExpression = (
value: string[] | string | number | boolean,
operator?: string,
): string => {
// For IN operators, ensure value is always an array
if (isArrayOperator(operator || '')) {
const arrayValue = Array.isArray(value) ? value : [value];
return `[${arrayValue
.map((v) =>
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
)
.join(', ')}]`;
}
if (Array.isArray(value)) {
// Handle array values (e.g., for IN operations)
return `[${value
.map((v) =>
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
)
.join(', ')}]`;
}
if (typeof value === 'string') {
// Add single quotes around all string values and escape internal single quotes
return `'${value.replace(/'/g, "\\'")}'`;
}
return String(value);
};
export const convertFiltersToExpression = (
filters: TagFilter,
): { expression: string } => {
if (!filters?.items || filters.items.length === 0) {
return { expression: '' };
}
const expressions = filters.items
.map((filter) => {
const { key, op, value } = filter;
// Skip if key is not defined
if (!key?.key) {
return '';
}
const formattedValue = formatValueForExpression(value, op);
return `${key.key} ${op} ${formattedValue}`;
})
.filter((expression) => expression !== ''); // Remove empty expressions
return {
expression: expressions.join(' AND '),
};
};
/**
* Convert old having format to new having format
* @param having - Array of old having objects with columnName, op, and value
* @returns New having format with expression string
*/
export const convertHavingToExpression = (
having: Having[],
): { expression: string } => {
if (!having || having.length === 0) {
return { expression: '' };
}
const expressions = having
.map((havingItem) => {
const { columnName, op, value } = havingItem;
// Skip if columnName is not defined
if (!columnName) {
return '';
}
// Format value based on its type
let formattedValue: string;
if (Array.isArray(value)) {
// For array values, format as [val1, val2, ...]
formattedValue = `[${value.join(', ')}]`;
} else {
// For single values, just convert to string
formattedValue = String(value);
}
return `${columnName} ${op} ${formattedValue}`;
})
.filter((expression) => expression !== ''); // Remove empty expressions
return {
expression: expressions.join(' AND '),
};
};
/**
* Convert old aggregation format to new aggregation format
* @param aggregateOperator - The aggregate operator (e.g., 'sum', 'count', 'avg')
* @param aggregateAttribute - The attribute to aggregate
* @param dataSource - The data source type
* @param timeAggregation - Time aggregation for metrics (optional)
* @param spaceAggregation - Space aggregation for metrics (optional)
* @param alias - Optional alias for the aggregation
* @returns New aggregation format based on data source
*
*/
export const convertAggregationToExpression = (
aggregateOperator: string,
aggregateAttribute: BaseAutocompleteData,
dataSource: DataSource,
timeAggregation?: string,
spaceAggregation?: string,
alias?: string,
): (TraceAggregation | LogAggregation | MetricAggregation)[] | undefined => {
// Skip if no operator or attribute key
if (!aggregateOperator) {
return undefined;
}
// Replace noop with count as default
const normalizedOperator =
aggregateOperator === 'noop' ? 'count' : aggregateOperator;
const normalizedTimeAggregation =
timeAggregation === 'noop' ? 'count' : timeAggregation;
const normalizedSpaceAggregation =
spaceAggregation === 'noop' ? 'count' : spaceAggregation;
// For metrics, use the MetricAggregation format
if (dataSource === DataSource.METRICS) {
return [
{
metricName: aggregateAttribute.key,
timeAggregation: (normalizedTimeAggregation || normalizedOperator) as any,
spaceAggregation: (normalizedSpaceAggregation || normalizedOperator) as any,
} as MetricAggregation,
];
}
// For traces and logs, use expression format
const expression = `${normalizedOperator}(${aggregateAttribute.key})`;
if (dataSource === DataSource.TRACES) {
return [
{
expression,
...(alias && { alias }),
} as TraceAggregation,
];
}
// For logs
return [
{
expression,
...(alias && { alias }),
} as LogAggregation,
];
};

View File

@@ -1,12 +1,7 @@
import { Tabs, TabsProps } from 'antd';
import { useLocation, useParams } from 'react-router-dom';
import { RouteTabProps } from './types';
interface Params {
[key: string]: string;
}
function RouteTab({
routes,
activeKey,
@@ -14,38 +9,19 @@ function RouteTab({
history,
...rest
}: RouteTabProps & TabsProps): JSX.Element {
const params = useParams<Params>();
const location = useLocation();
// Replace dynamic parameters in routes
const routesWithParams = routes.map((route) => ({
...route,
route: route.route.replace(
/:(\w+)/g,
(match, param) => params[param] || match,
),
}));
// Find the matching route for the current pathname
const currentRoute = routesWithParams.find((route) => {
const routePattern = route.route.replace(/:(\w+)/g, '([^/]+)');
const regex = new RegExp(`^${routePattern}$`);
return regex.test(location.pathname);
});
const onChange = (activeRoute: string): void => {
if (onChangeHandler) {
onChangeHandler(activeRoute);
}
const selectedRoute = routesWithParams.find((e) => e.key === activeRoute);
const selectedRoute = routes.find((e) => e.key === activeRoute);
if (selectedRoute) {
history.push(selectedRoute.route);
}
};
const items = routesWithParams.map(({ Component, name, route, key }) => ({
const items = routes.map(({ Component, name, route, key }) => ({
label: name,
key,
tabKey: route,
@@ -56,8 +32,8 @@ function RouteTab({
<Tabs
onChange={onChange}
destroyInactiveTabPane
activeKey={currentRoute?.key || activeKey}
defaultActiveKey={currentRoute?.key || activeKey}
activeKey={activeKey}
defaultActiveKey={activeKey}
animated
items={items}
// eslint-disable-next-line react/jsx-props-no-spreading

View File

@@ -15,4 +15,3 @@ export const DASHBOARD_TIME_IN_DURATION = 'refreshInterval';
export const DEFAULT_ENTITY_VERSION = 'v3';
export const ENTITY_VERSION_V4 = 'v4';
export const ENTITY_VERSION_V5 = 'v5';

View File

@@ -30,5 +30,5 @@ export enum LOCALSTORAGE {
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
BANNER_DISMISSED = 'BANNER_DISMISSED',
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
FUNNEL_STEPS = 'FUNNEL_STEPS',
UNEXECUTED_FUNNELS = 'UNEXECUTED_FUNNELS',
}

View File

@@ -1,18 +0,0 @@
export const ORG_PREFERENCES = {
ORG_ONBOARDING: 'org_onboarding',
WELCOME_CHECKLIST_DO_LATER: 'welcome_checklist_do_later',
WELCOME_CHECKLIST_SEND_LOGS_SKIPPED: 'welcome_checklist_send_logs_skipped',
WELCOME_CHECKLIST_SEND_TRACES_SKIPPED: 'welcome_checklist_send_traces_skipped',
WELCOME_CHECKLIST_SETUP_ALERTS_SKIPPED:
'welcome_checklist_setup_alerts_skipped',
WELCOME_CHECKLIST_SETUP_SAVED_VIEW_SKIPPED:
'welcome_checklist_setup_saved_view_skipped',
WELCOME_CHECKLIST_SEND_INFRA_METRICS_SKIPPED:
'welcome_checklist_send_infra_metrics_skipped',
WELCOME_CHECKLIST_SETUP_DASHBOARDS_SKIPPED:
'welcome_checklist_setup_dashboards_skipped',
WELCOME_CHECKLIST_SETUP_WORKSPACE_SKIPPED:
'welcome_checklist_setup_workspace_skipped',
WELCOME_CHECKLIST_ADD_DATA_SOURCE_SKIPPED:
'welcome_checklist_add_data_source_skipped',
};

View File

@@ -46,5 +46,4 @@ export enum QueryParams {
msgSystem = 'msgSystem',
destination = 'destination',
kindString = 'kindString',
selectedExplorerView = 'selectedExplorerView',
}

View File

@@ -169,9 +169,6 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
aggregateAttribute: initialAutocompleteData,
timeAggregation: MetricAggregateOperator.RATE,
spaceAggregation: MetricAggregateOperator.SUM,
filter: { expression: '' },
aggregations: [{ expression: 'count() ' }],
havingExpression: { expression: '' },
functions: [],
filters: { items: [], op: 'AND' },
expression: createNewBuilderItemName({

View File

@@ -6,6 +6,10 @@ import {
import { SelectOption } from 'types/common/select';
export const metricAggregateOperatorOptions: SelectOption<string, string>[] = [
{
value: MetricAggregateOperator.NOOP,
label: 'NOOP',
},
{
value: MetricAggregateOperator.COUNT,
label: 'Count',
@@ -126,6 +130,10 @@ export const metricAggregateOperatorOptions: SelectOption<string, string>[] = [
];
export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
{
value: TracesAggregatorOperator.NOOP,
label: 'NOOP',
},
{
value: TracesAggregatorOperator.COUNT,
label: 'Count',
@@ -209,6 +217,10 @@ export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
];
export const logsAggregateOperatorOptions: SelectOption<string, string>[] = [
{
value: LogsAggregatorOperator.NOOP,
label: 'NOOP',
},
{
value: LogsAggregatorOperator.COUNT,
label: 'Count',

View File

@@ -29,12 +29,12 @@ const ROUTES = {
ALERT_OVERVIEW: '/alerts/overview',
ALL_CHANNELS: '/settings/channels',
CHANNELS_NEW: '/settings/channels/new',
CHANNELS_EDIT: '/settings/channels/edit/:channelId',
CHANNELS_EDIT: '/settings/channels/:id',
ALL_ERROR: '/exceptions',
ERROR_DETAIL: '/error-detail',
VERSION: '/status',
MY_SETTINGS: '/my-settings',
SETTINGS: '/settings',
MY_SETTINGS: '/settings/my-settings',
ORG_SETTINGS: '/settings/org-settings',
CUSTOM_DOMAIN_SETTINGS: '/settings/custom-domain-settings',
API_KEYS: '/settings/api-keys',
@@ -52,7 +52,7 @@ const ROUTES = {
LIST_LICENSES: '/licenses',
LOGS_INDEX_FIELDS: '/logs-explorer/index-fields',
TRACE_EXPLORER: '/trace-explorer',
BILLING: '/settings/billing',
BILLING: '/billing',
SUPPORT: '/support',
LOGS_SAVE_VIEWS: '/logs/saved-views',
TRACES_SAVE_VIEWS: '/traces/saved-views',
@@ -60,12 +60,10 @@ const ROUTES = {
TRACES_FUNNELS_DETAIL: '/traces/funnels/:funnelId',
WORKSPACE_LOCKED: '/workspace-locked',
WORKSPACE_SUSPENDED: '/workspace-suspended',
SHORTCUTS: '/settings/shortcuts',
SHORTCUTS: '/shortcuts',
INTEGRATIONS: '/integrations',
MESSAGING_QUEUES_BASE: '/messaging-queues',
MESSAGING_QUEUES_KAFKA: '/messaging-queues/kafka',
MESSAGING_QUEUES_KAFKA_DETAIL: '/messaging-queues/kafka/detail',
INFRASTRUCTURE_MONITORING_BASE: '/infrastructure-monitoring',
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes',
MESSAGING_QUEUES_CELERY_TASK: '/messaging-queues/celery-task',
@@ -73,7 +71,6 @@ const ROUTES = {
METRICS_EXPLORER: '/metrics-explorer/summary',
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
API_MONITORING_BASE: '/api-monitoring',
API_MONITORING: '/api-monitoring/explorer',
METRICS_EXPLORER_BASE: '/metrics-explorer',
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',

View File

@@ -1,4 +0,0 @@
export const USER_PREFERENCES = {
SIDENAV_PINNED: 'sidenav_pinned',
NAV_SHORTCUTS: 'nav_shortcuts',
};

View File

@@ -21,9 +21,9 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
const [action] = useComponentPermission(['new_alert_action'], user.role);
const onClickEditHandler = useCallback((id: string) => {
history.push(
history.replace(
generatePath(ROUTES.CHANNELS_EDIT, {
channelId: id,
id,
}),
);
}, []);

View File

@@ -1,4 +0,0 @@
.alert-channels-container {
width: 90%;
margin: 12px auto;
}

View File

@@ -120,19 +120,14 @@ describe('Create Alert Channel', () => {
expect(screen.getByText('button_test_channel')).toBeInTheDocument();
expect(screen.getByText('button_return')).toBeInTheDocument();
});
it('Should check if saving the form without filling the name displays error notification', async () => {
it('Should check if saving the form without filling the name displays "Something went wrong"', async () => {
const saveButton = screen.getByRole('button', {
name: 'button_save_channel',
});
fireEvent.click(saveButton);
await waitFor(() =>
expect(errorNotification).toHaveBeenCalledWith({
message: 'Error',
description: 'channel_name_required',
}),
);
await waitFor(() => expect(showErrorModal).toHaveBeenCalled());
});
it('Should check if clicking on Test button shows "An alert has been sent to this channel" success message if testing passes', async () => {
server.use(

View File

@@ -1,5 +1,3 @@
import './AllAlertChannels.styles.scss';
import { PlusOutlined } from '@ant-design/icons';
import { Tooltip, Typography } from 'antd';
import getAll from 'api/channels/getAll';
@@ -58,7 +56,7 @@ function AlertChannels(): JSX.Element {
}
return (
<div className="alert-channels-container">
<>
<ButtonContainer>
<Paragraph ellipsis type="secondary">
{t('sending_channels_note')}
@@ -89,7 +87,7 @@ function AlertChannels(): JSX.Element {
</ButtonContainer>
<AlertChannelsComponent allChannels={data?.data || []} />
</div>
</>
);
}

View File

@@ -54,21 +54,19 @@ function DomainList(): JSX.Element {
// initialise tab with default query.
useShareBuilderUrl({
defaultValue: {
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [
{
...initialQueriesMap.traces.builder.queryData[0],
dataSource: DataSource.TRACES,
aggregateOperator: 'noop',
aggregateAttribute: {
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
},
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [
{
...initialQueriesMap.traces.builder.queryData[0],
dataSource: DataSource.TRACES,
aggregateOperator: 'noop',
aggregateAttribute: {
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
},
],
},
},
],
},
});

View File

@@ -1,34 +1,8 @@
.app-banner-container {
position: relative;
width: 100%;
}
.app-layout {
position: relative;
height: 100%;
width: 100%;
&.isWorkspaceRestricted {
height: calc(100% - 32px);
// same styles as its either trial expired or payment failed
&.isTrialExpired {
height: calc(100% - 64px);
}
&.isPaymentFailed {
height: calc(100% - 64px);
}
}
&.isTrialExpired {
height: calc(100% - 32px);
}
&.isPaymentFailed {
height: calc(100% - 32px);
}
.app-content {
width: calc(100% - 64px); // width of the sidebar
z-index: 0;
@@ -48,12 +22,6 @@
width: 100%;
}
}
&.side-nav-pinned {
.app-content {
width: calc(100% - 240px);
}
}
}
.chat-support-gateway {
@@ -189,9 +157,3 @@
text-underline-offset: 2px;
}
}
.workspace-restricted-banner,
.trial-expiry-banner,
.payment-failed-banner {
height: 32px;
}

View File

@@ -7,7 +7,6 @@ import * as Sentry from '@sentry/react';
import { Flex } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import getChangelogByVersion from 'api/changelog/getChangelogByVersion';
import logEvent from 'api/common/logEvent';
import manageCreditCardApi from 'api/v1/portal/create';
import getUserLatestVersion from 'api/v1/version/getLatestVersion';
@@ -19,7 +18,6 @@ import { Events } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import { USER_PREFERENCES } from 'constants/userPreferences';
import SideNav from 'container/SideNav';
import TopNav from 'container/TopNav';
import dayjs from 'dayjs';
@@ -29,6 +27,7 @@ import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { isNull } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { INTEGRATION_TYPES } from 'pages/Integrations/utils';
import { useAppContext } from 'providers/App/App';
import {
ReactNode,
@@ -41,10 +40,9 @@ import {
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
import { useMutation, useQueries } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { matchPath, useLocation } from 'react-router-dom';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
UPDATE_CURRENT_ERROR,
@@ -52,18 +50,15 @@ import {
UPDATE_LATEST_VERSION,
UPDATE_LATEST_VERSION_ERROR,
} from 'types/actions/app';
import { ErrorResponse, SuccessResponse, SuccessResponseV2 } from 'types/api';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { ChangelogSchema } from 'types/api/changelog/getChangelogByVersion';
import APIError from 'types/api/error';
import {
LicenseEvent,
LicensePlatform,
LicenseState,
} from 'types/api/licensesV3/getActive';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { checkVersionState } from 'utils/app';
import { eventEmitter } from 'utils/getEventEmitter';
import {
getFormattedDate,
@@ -85,8 +80,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
userPreferences,
updateChangelog,
} = useAppContext();
const { notifications } = useNotifications();
@@ -98,15 +91,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const [showSlowApiWarning, setShowSlowApiWarning] = useState(false);
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
const [shouldFetchChangelog, setShouldFetchChangelog] = useState<boolean>(
false,
);
const { currentVersion, latestVersion } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const isLatestVersion = checkVersionState(currentVersion, latestVersion);
const handleBillingOnSuccess = (
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
@@ -144,11 +128,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const [
getUserVersionResponse,
getUserLatestVersionResponse,
getChangelogByVersionResponse,
] = useQueries([
const [getUserVersionResponse, getUserLatestVersionResponse] = useQueries([
{
queryFn: getUserVersion,
queryKey: ['getUserVersion', user?.accessJwt],
@@ -159,12 +139,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
queryKey: ['getUserLatestVersion', user?.accessJwt],
enabled: isLoggedIn,
},
{
queryFn: (): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> =>
getChangelogByVersion(latestVersion),
queryKey: ['getChangelogByVersion', latestVersion],
enabled: isLoggedIn && !isCloudUserVal && shouldFetchChangelog,
},
]);
useEffect(() => {
@@ -267,30 +241,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
notifications,
]);
useEffect(() => {
if (!isLatestVersion) {
setShouldFetchChangelog(true);
}
}, [isLatestVersion]);
useEffect(() => {
if (
getChangelogByVersionResponse.isFetched &&
getChangelogByVersionResponse.isSuccess &&
getChangelogByVersionResponse.data &&
getChangelogByVersionResponse.data.payload
) {
updateChangelog(getChangelogByVersionResponse.data.payload);
}
}, [
updateChangelog,
getChangelogByVersionResponse.isFetched,
getChangelogByVersionResponse.isLoading,
getChangelogByVersionResponse.isError,
getChangelogByVersionResponse.data,
getChangelogByVersionResponse.isSuccess,
]);
const isToDisplayLayout = isLoggedIn;
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
@@ -380,6 +330,53 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
});
}, [manageCreditCard]);
const isHome = (): boolean => routeKey === 'HOME';
const isLogsView = (): boolean =>
routeKey === 'LOGS' ||
routeKey === 'LOGS_EXPLORER' ||
routeKey === 'LOGS_PIPELINES' ||
routeKey === 'LOGS_SAVE_VIEWS';
const isApiMonitoringView = (): boolean => routeKey === 'API_MONITORING';
const isExceptionsView = (): boolean => routeKey === 'ALL_ERROR';
const isTracesView = (): boolean =>
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
const isMessagingQueues = (): boolean =>
routeKey === 'MESSAGING_QUEUES_KAFKA' ||
routeKey === 'MESSAGING_QUEUES_KAFKA_DETAIL' ||
routeKey === 'MESSAGING_QUEUES_CELERY_TASK' ||
routeKey === 'MESSAGING_QUEUES_OVERVIEW';
const isCloudIntegrationPage = (): boolean =>
routeKey === 'INTEGRATIONS' &&
new URLSearchParams(window.location.search).get('integration') ===
INTEGRATION_TYPES.AWS_INTEGRATION;
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW';
const isInfraMonitoring = (): boolean =>
routeKey === 'INFRASTRUCTURE_MONITORING_HOSTS' ||
routeKey === 'INFRASTRUCTURE_MONITORING_KUBERNETES';
const isTracesFunnels = (): boolean => routeKey === 'TRACES_FUNNELS';
const isTracesFunnelDetails = (): boolean =>
!!matchPath(pathname, ROUTES.TRACES_FUNNELS_DETAIL);
const isPathMatch = (regex: RegExp): boolean => regex.test(pathname);
const isDashboardView = (): boolean =>
isPathMatch(/^\/dashboard\/[a-zA-Z0-9_-]+$/);
const isDashboardWidgetView = (): boolean =>
isPathMatch(/^\/dashboard\/[a-zA-Z0-9_-]+\/new$/);
const isTraceDetailsView = (): boolean =>
isPathMatch(/^\/trace\/[a-zA-Z0-9]+(\?.*)?$/);
useEffect(() => {
if (isDarkMode) {
document.body.classList.remove('lightMode');
@@ -596,84 +593,61 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</div>
);
const sideNavPinned = userPreferences?.find(
(preference) => preference.name === USER_PREFERENCES.SIDENAV_PINNED,
)?.value as boolean;
const SHOW_TRIAL_EXPIRY_BANNER =
showTrialExpiryBanner && !showPaymentFailedWarning;
const SHOW_WORKSPACE_RESTRICTED_BANNER = showWorkspaceRestricted;
const SHOW_PAYMENT_FAILED_BANNER =
!showTrialExpiryBanner && showPaymentFailedWarning;
return (
<Layout className={cx(isDarkMode ? 'darkMode dark' : 'lightMode')}>
<Helmet>
<title>{pageTitle}</title>
</Helmet>
{isLoggedIn && (
<div className={cx('app-banner-container')}>
{SHOW_TRIAL_EXPIRY_BANNER && (
<div className="trial-expiry-banner">
You are in free trial period. Your free trial will end on{' '}
<span>{getFormattedDate(trialInfo?.trialEnd || Date.now())}.</span>
{user.role === USER_ROLES.ADMIN ? (
<span>
{' '}
Please{' '}
<a className="upgrade-link" onClick={handleUpgrade}>
upgrade
</a>
to continue using SigNoz features.
</span>
) : (
'Please contact your administrator for upgrading to a paid plan.'
)}
</div>
{showTrialExpiryBanner && !showPaymentFailedWarning && (
<div className="trial-expiry-banner">
You are in free trial period. Your free trial will end on{' '}
<span>{getFormattedDate(trialInfo?.trialEnd || Date.now())}.</span>
{user.role === USER_ROLES.ADMIN ? (
<span>
{' '}
Please{' '}
<a className="upgrade-link" onClick={handleUpgrade}>
upgrade
</a>
to continue using SigNoz features.
</span>
) : (
'Please contact your administrator for upgrading to a paid plan.'
)}
</div>
)}
{SHOW_WORKSPACE_RESTRICTED_BANNER && renderWorkspaceRestrictedBanner()}
{showWorkspaceRestricted && renderWorkspaceRestrictedBanner()}
{SHOW_PAYMENT_FAILED_BANNER && (
<div className="payment-failed-banner">
Your bill payment has failed. Your workspace will get suspended on{' '}
<span>
{getFormattedDateWithMinutes(
dayjs(activeLicense?.event_queue?.scheduled_at).unix() || Date.now(),
)}
.
</span>
{user.role === USER_ROLES.ADMIN ? (
<span>
{' '}
Please{' '}
<a className="upgrade-link" onClick={handleFailedPayment}>
pay the bill
</a>
to continue using SigNoz features.
</span>
) : (
' Please contact your administrator to pay the bill.'
)}
</div>
{!showTrialExpiryBanner && showPaymentFailedWarning && (
<div className="payment-failed-banner">
Your bill payment has failed. Your workspace will get suspended on{' '}
<span>
{getFormattedDateWithMinutes(
dayjs(activeLicense?.event_queue?.scheduled_at).unix() || Date.now(),
)}
.
</span>
{user.role === USER_ROLES.ADMIN ? (
<span>
{' '}
Please{' '}
<a className="upgrade-link" onClick={handleFailedPayment}>
pay the bill
</a>
to continue using SigNoz features.
</span>
) : (
' Please contact your administrator to pay the bill.'
)}
</div>
)}
<Flex
className={cx(
'app-layout',
isDarkMode ? 'darkMode dark' : 'lightMode',
sideNavPinned ? 'side-nav-pinned' : '',
SHOW_WORKSPACE_RESTRICTED_BANNER ? 'isWorkspaceRestricted' : '',
SHOW_TRIAL_EXPIRY_BANNER ? 'isTrialExpired' : '',
SHOW_PAYMENT_FAILED_BANNER ? 'isPaymentFailed' : '',
)}
className={cx('app-layout', isDarkMode ? 'darkMode dark' : 'lightMode')}
>
{isToDisplayLayout && !renderFullScreen && (
<SideNav isPinned={sideNavPinned} />
)}
{isToDisplayLayout && !renderFullScreen && <SideNav />}
<div
className={cx('app-content', {
'full-screen-content': renderFullScreen,
@@ -683,7 +657,32 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<LayoutContent data-overlayscrollbars-initialize>
<OverlayScrollbar>
<ChildrenContainer>
<ChildrenContainer
style={{
margin:
isHome() ||
isLogsView() ||
isTracesView() ||
isDashboardView() ||
isDashboardWidgetView() ||
isDashboardListView() ||
isAlertHistory() ||
isAlertOverview() ||
isMessagingQueues() ||
isCloudIntegrationPage() ||
isInfraMonitoring() ||
isApiMonitoringView() ||
isExceptionsView()
? 0
: '0 1rem',
...(isTraceDetailsView() ||
isTracesFunnels() ||
isTracesFunnelDetails()
? { margin: 0 }
: {}),
}}
>
{isToDisplayLayout && !renderFullScreen && <TopNav />}
{children}
</ChildrenContainer>

View File

@@ -1,8 +1,7 @@
.billing-container {
margin-bottom: 40px;
padding-top: 36px;
width: 90%;
margin: 0 auto;
width: 65%;
.billing-summary {
margin: 24px 8px;

View File

@@ -14,7 +14,7 @@ function CloudIntegrationPage(): JSX.Element {
<HeroSection />
<RequestIntegrationBtn
type={IntegrationType.AWS_SERVICES}
message="Can't find the AWS service you're looking for? Request more integrations"
message="Cannot find the AWS service you're looking for? Request more integrations"
/>
<ServicesTabs />
</div>

View File

@@ -60,30 +60,26 @@ function CloudServiceDataCollected({
return (
<div className="cloud-service-data-collected">
{logsData && logsData.length > 0 && (
<div className="cloud-service-data-collected__table">
<div className="cloud-service-data-collected__table-heading">Logs</div>
<Table
columns={logsColumns}
dataSource={logsData}
// eslint-disable-next-line react/jsx-props-no-spreading
{...tableProps}
className="cloud-service-data-collected__table-logs"
/>
</div>
)}
{metricsData && metricsData.length > 0 && (
<div className="cloud-service-data-collected__table">
<div className="cloud-service-data-collected__table-heading">Metrics</div>
<Table
columns={metricsColumns}
dataSource={metricsData}
// eslint-disable-next-line react/jsx-props-no-spreading
{...tableProps}
className="cloud-service-data-collected__table-metrics"
/>
</div>
)}
<div className="cloud-service-data-collected__table">
<div className="cloud-service-data-collected__table-heading">Logs</div>
<Table
columns={logsColumns}
dataSource={logsData}
// eslint-disable-next-line react/jsx-props-no-spreading
{...tableProps}
className="cloud-service-data-collected__table-logs"
/>
</div>
<div className="cloud-service-data-collected__table">
<div className="cloud-service-data-collected__table-heading">Metrics</div>
<Table
columns={metricsColumns}
dataSource={metricsData}
// eslint-disable-next-line react/jsx-props-no-spreading
{...tableProps}
className="cloud-service-data-collected__table-metrics"
/>
</div>
</div>
);
}

View File

@@ -51,33 +51,6 @@ function ServiceStatus({
return <div className={`service-status ${className}`}>{text}</div>;
}
function getTabItems(serviceDetailsData: any): TabsProps['items'] {
const dashboards = serviceDetailsData?.assets.dashboards || [];
const dataCollected = serviceDetailsData?.data_collected || {};
const items: TabsProps['items'] = [];
if (dashboards.length) {
items.push({
key: 'dashboards',
label: `Dashboards (${dashboards.length})`,
children: <CloudServiceDashboards service={serviceDetailsData} />,
});
}
items.push({
key: 'data-collected',
label: 'Data Collected',
children: (
<CloudServiceDataCollected
logsData={dataCollected.logs || []}
metricsData={dataCollected.metrics || []}
/>
),
});
return items;
}
function ServiceDetails(): JSX.Element | null {
const urlQuery = useUrlQuery();
const cloudAccountId = urlQuery.get('cloudAccountId');
@@ -133,7 +106,23 @@ function ServiceDetails(): JSX.Element | null {
return null;
}
const tabItems = getTabItems(serviceDetailsData);
const tabItems: TabsProps['items'] = [
{
key: 'dashboards',
label: `Dashboards (${serviceDetailsData?.assets.dashboards.length})`,
children: <CloudServiceDashboards service={serviceDetailsData} />,
},
{
key: 'data-collected',
label: 'Data Collected',
children: (
<CloudServiceDataCollected
logsData={serviceDetailsData?.data_collected.logs || []}
metricsData={serviceDetailsData?.data_collected.metrics || []}
/>
),
},
];
return (
<div className="service-details">

View File

@@ -34,7 +34,7 @@ describe('Request AWS integration', () => {
expect(
screen.getByText(
/can't find what youre looking for\? request more integrations/i,
/cannot find what youre looking for\? request more integrations/i,
),
).toBeInTheDocument();

View File

@@ -1,15 +0,0 @@
.create-alert-channels-container {
width: 90%;
margin: 12px auto;
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
border-radius: 3px;
padding: 16px;
.form-alert-channels-title {
margin-top: 0px;
margin-bottom: 16px;
}
}

View File

@@ -1,5 +1,3 @@
import './CreateAlertChannels.styles.scss';
import { Form } from 'antd';
import createEmail from 'api/channels/createEmail';
import createMsTeamsApi from 'api/channels/createMsTeams';
@@ -138,14 +136,6 @@ function CreateAlertChannels({
);
const onSlackHandler = useCallback(async () => {
if (!selectedConfig.api_url) {
notifications.error({
message: 'Error',
description: t('webhook_url_required'),
});
return;
}
setSavingState(true);
try {
@@ -162,7 +152,7 @@ function CreateAlertChannels({
} finally {
setSavingState(false);
}
}, [selectedConfig, notifications, t, prepareSlackRequest, showErrorModal]);
}, [prepareSlackRequest, notifications, t, showErrorModal]);
const prepareWebhookRequest = useCallback(() => {
// initial api request without auth params
@@ -200,14 +190,6 @@ function CreateAlertChannels({
}, [notifications, t, selectedConfig]);
const onWebhookHandler = useCallback(async () => {
if (!selectedConfig.api_url) {
notifications.error({
message: 'Error',
description: t('webhook_url_required'),
});
return;
}
setSavingState(true);
try {
const request = prepareWebhookRequest();
@@ -224,13 +206,7 @@ function CreateAlertChannels({
} finally {
setSavingState(false);
}
}, [
selectedConfig.api_url,
notifications,
t,
prepareWebhookRequest,
showErrorModal,
]);
}, [prepareWebhookRequest, notifications, t, showErrorModal]);
const preparePagerRequest = useCallback(() => {
const validationError = ValidatePagerChannel(selectedConfig as PagerChannel);
@@ -294,14 +270,6 @@ function CreateAlertChannels({
);
const onOpsgenieHandler = useCallback(async () => {
if (!selectedConfig.api_key) {
notifications.error({
message: 'Error',
description: t('api_key_required'),
});
return;
}
setSavingState(true);
try {
await createOpsgenie(prepareOpsgenieRequest());
@@ -317,13 +285,7 @@ function CreateAlertChannels({
} finally {
setSavingState(false);
}
}, [
selectedConfig.api_key,
notifications,
t,
prepareOpsgenieRequest,
showErrorModal,
]);
}, [prepareOpsgenieRequest, notifications, t, showErrorModal]);
const prepareEmailRequest = useCallback(
() => ({
@@ -337,14 +299,6 @@ function CreateAlertChannels({
);
const onEmailHandler = useCallback(async () => {
if (!selectedConfig.to) {
notifications.error({
message: 'Error',
description: t('to_required'),
});
return;
}
setSavingState(true);
try {
const request = prepareEmailRequest();
@@ -361,7 +315,7 @@ function CreateAlertChannels({
} finally {
setSavingState(false);
}
}, [prepareEmailRequest, notifications, t, showErrorModal, selectedConfig.to]);
}, [prepareEmailRequest, notifications, t, showErrorModal]);
const prepareMsTeamsRequest = useCallback(
() => ({
@@ -375,14 +329,6 @@ function CreateAlertChannels({
);
const onMsTeamsHandler = useCallback(async () => {
if (!selectedConfig.webhook_url) {
notifications.error({
message: 'Error',
description: t('webhook_url_required'),
});
return;
}
setSavingState(true);
try {
@@ -399,24 +345,10 @@ function CreateAlertChannels({
} finally {
setSavingState(false);
}
}, [
selectedConfig.webhook_url,
notifications,
t,
prepareMsTeamsRequest,
showErrorModal,
]);
}, [prepareMsTeamsRequest, notifications, t, showErrorModal]);
const onSaveHandler = useCallback(
async (value: ChannelType) => {
if (!selectedConfig.name) {
notifications.error({
message: 'Error',
description: t('channel_name_required'),
});
return;
}
const functionMapper = {
[ChannelType.Slack]: onSlackHandler,
[ChannelType.Webhook]: onWebhookHandler,
@@ -545,28 +477,26 @@ function CreateAlertChannels({
);
return (
<div className="create-alert-channels-container">
<FormAlertChannels
{...{
formInstance,
onTypeChangeHandler,
setSelectedConfig,
<FormAlertChannels
{...{
formInstance,
onTypeChangeHandler,
setSelectedConfig,
type,
onTestHandler,
onSaveHandler,
savingState,
testingState,
title: t('page_title_create'),
initialValue: {
type,
onTestHandler,
onSaveHandler,
savingState,
testingState,
title: t('page_title_create'),
initialValue: {
type,
...selectedConfig,
...PagerInitialConfig,
...OpsgenieInitialConfig,
...EmailInitialConfig,
},
}}
/>
</div>
...selectedConfig,
...PagerInitialConfig,
...OpsgenieInitialConfig,
...EmailInitialConfig,
},
}}
/>
);
}

View File

@@ -69,7 +69,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
}
>
<Button
className="periscope-btn ghost"
className="periscope-btn"
loading={isLoading}
icon={<FileDown size={14} />}
/>

View File

@@ -28,6 +28,7 @@ import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import APIError from 'types/api/error';
function EditAlertChannels({
@@ -52,11 +53,7 @@ function EditAlertChannels({
const [savingState, setSavingState] = useState<boolean>(false);
const [testingState, setTestingState] = useState<boolean>(false);
const { notifications } = useNotifications();
// Extract channelId from URL pathname since useParams doesn't work in nested routing
const { pathname } = window.location;
const channelIdMatch = pathname.match(/\/settings\/channels\/edit\/([^/]+)/);
const id = channelIdMatch ? channelIdMatch[1] : '';
const { id } = useParams<{ id: string }>();
const [type, setType] = useState<ChannelType>(
initialValue?.type ? (initialValue.type as ChannelType) : ChannelType.Slack,

View File

@@ -1,14 +1,12 @@
.explorer-options-container {
position: fixed;
bottom: 8px;
bottom: 24px;
left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear;
border-radius: 6px;
background: var(--Ink-300, #16181d);
display: flex;
gap: 16px;
background-color: transparent;
.multi-alert-button,
@@ -34,15 +32,19 @@
.explorer-update {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 8px;
background: var(--Ink-300, #16181d);
gap: 12px;
padding: 10px 10px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: rgba(22, 24, 29, 0.6);
backdrop-filter: blur(20px);
.action-icon {
display: flex;
justify-content: center;
align-items: center;
padding: 6px;
padding: 8px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
cursor: pointer;
@@ -62,8 +64,10 @@
.explorer-options {
padding: 10px 12px;
background: var(--Ink-300, #16181d);
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
border-radius: 50px;
background: rgba(22, 24, 29, 0.6);
backdrop-filter: blur(20px);
cursor: default;
display: flex;
@@ -92,6 +96,27 @@
align-items: center;
gap: 8px;
button {
display: flex;
justify-content: center;
align-items: center;
border: none;
border: 1px solid #1d2023;
box-shadow: none !important;
&.ant-btn-round {
padding: 8px 12px 8px 10px;
font-weight: 500;
}
&.ant-btn-round:disabled {
background-color: rgba(209, 209, 209, 0.074);
color: #5f5f5f;
}
}
.ant-select-focused {
border-color: transparent !important;
@@ -232,10 +257,6 @@
}
.lightMode {
.explorer-options-container {
background: var(--bg-vanilla-100);
}
.explorer-options {
background: transparent;
box-shadow: none;

View File

@@ -1,10 +1,12 @@
/* eslint-disable react/jsx-props-no-spreading */
import './ExplorerOptions.styles.scss';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import {
Button,
ColorPicker,
Divider,
Input,
Modal,
RefSelectProps,
@@ -13,7 +15,6 @@ import {
Typography,
} from 'antd';
import logEvent from 'api/common/logEvent';
import { TelemetryFieldKey } from 'api/v5/v5';
import axios from 'axios';
import cx from 'classnames';
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
@@ -44,7 +45,14 @@ import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { cloneDeep, isEqual, omit } from 'lodash-es';
import { Check, ConciergeBell, Disc3, Plus, X } from 'lucide-react';
import {
Check,
ConciergeBell,
Disc3,
PanelBottomClose,
Plus,
X,
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { FormattingOptions } from 'providers/preferences/types';
import {
@@ -59,6 +67,7 @@ import {
} from 'react';
import { useHistory } from 'react-router-dom';
import { Dashboard } from 'types/api/dashboard/getAll';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { ViewProps } from 'types/api/saveViews/types';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
@@ -69,8 +78,10 @@ import ExplorerOptionsHideArea from './ExplorerOptionsHideArea';
import { PreservedViewsInLocalStorage } from './types';
import {
DATASOURCE_VS_ROUTES,
generateRGBAFromHex,
getRandomColor,
saveNewViewHandler,
setExplorerToolBarVisibility,
} from './utils';
const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR];
@@ -242,6 +253,12 @@ function ExplorerOptions({
const extraData = viewsData?.data?.data?.find((view) => view.id === viewKey)
?.extraData;
const extraDataColor = extraData ? JSON.parse(extraData).color : '';
const rgbaColor = generateRGBAFromHex(
extraDataColor || Color.BG_SIENNA_500,
0.08,
);
const { options, handleOptionsChange } = useOptionsMenu({
storageKey:
sourcepage === DataSource.TRACES
@@ -253,7 +270,7 @@ function ExplorerOptions({
const getUpdatedExtraData = (
extraData: string | undefined,
newSelectedColumns: TelemetryFieldKey[],
newSelectedColumns: BaseAutocompleteData[],
formattingOptions?: FormattingOptions,
): string => {
let updatedExtraData;
@@ -337,7 +354,7 @@ function ExplorerOptions({
const { handleExplorerTabChange } = useHandleExplorerTabChange();
type ExtraData = {
selectColumns?: TelemetryFieldKey[];
selectColumns?: BaseAutocompleteData[];
version?: number;
};
@@ -572,6 +589,13 @@ function ExplorerOptions({
[isDarkMode],
);
const hideToolbar = (): void => {
setExplorerToolBarVisibility(false, sourcepage);
if (setIsExplorerOptionHidden) {
setIsExplorerOptionHidden(true);
}
};
const isEditDeleteSupported = allowedRoles.includes(user.role as string);
const [
@@ -623,6 +647,27 @@ function ExplorerOptions({
viewsData?.data?.data,
]);
const infoIconText = useMemo(() => {
if (isLogsExplorer) {
return 'Learn more about Logs explorer';
}
if (isMetricsExplorer) {
return 'Learn more about Metrics explorer';
}
return 'Learn more about Traces explorer';
}, [isLogsExplorer, isMetricsExplorer]);
const infoIconLink = useMemo(() => {
if (isLogsExplorer) {
return 'https://signoz.io/docs/product-features/logs-explorer/?utm_source=product&utm_medium=logs-explorer-toolbar';
}
// TODO: Add metrics explorer info icon link
if (isMetricsExplorer) {
return '';
}
return 'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=trace-explorer-toolbar';
}, [isLogsExplorer, isMetricsExplorer]);
const getQueryName = (query: Query): string => {
if (query.builder.queryFormulas.length > 0) {
return `Formula ${query.builder.queryFormulas[0].queryName}`;
@@ -635,10 +680,11 @@ function ExplorerOptions({
const selectLabel = (
<Button
disabled={disabled}
className="periscope-btn ghost"
shape="default"
shape="round"
icon={<ConciergeBell size={16} />}
/>
>
Create an Alert
</Button>
);
return (
<Select
@@ -667,11 +713,12 @@ function ExplorerOptions({
return (
<Button
disabled={disabled}
shape="default"
className="periscope-btn ghost"
shape="round"
onClick={(): void => onCreateAlertsHandler(query)}
icon={<ConciergeBell size={16} />}
/>
>
Create an Alert
</Button>
);
}, [
disabled,
@@ -685,11 +732,14 @@ function ExplorerOptions({
if (isOneChartPerQuery) {
const selectLabel = (
<Button
className="periscope-btn ghost"
type="primary"
disabled={disabled}
shape="round"
onClick={onAddToDashboard}
icon={<Plus size={12} />}
/>
icon={<Plus size={16} />}
>
Add to Dashboard
</Button>
);
return (
<Select
@@ -721,11 +771,14 @@ function ExplorerOptions({
}
return (
<Button
className="periscope-btn ghost"
type="primary"
disabled={disabled}
shape="round"
onClick={onAddToDashboard}
icon={<Plus size={16} />}
/>
>
Add to Dashboard
</Button>
);
}, [disabled, isOneChartPerQuery, onAddToDashboard, splitedQueries]);
@@ -744,31 +797,41 @@ function ExplorerOptions({
>
<Tooltip title="Clear this view" placement="top">
<Button
className="periscope-btn ghost"
className="action-icon"
onClick={handleClearSelect}
icon={<X size={16} />}
icon={<X size={14} />}
/>
</Tooltip>
{
// only show the update view option when the query is updated
}
{isQueryUpdated && (
<Tooltip title="Update this view" placement="top">
<Button
className={cx(
'periscope-btn ghost',
isEditDeleteSupported ? '' : 'hidden',
)}
disabled={isViewUpdating}
onClick={onUpdateQueryHandler}
icon={<Disc3 size={16} />}
<>
<Divider
type="vertical"
className={isEditDeleteSupported ? '' : 'hidden'}
/>
</Tooltip>
<Tooltip title="Update this view" placement="top">
<Button
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
disabled={isViewUpdating}
onClick={onUpdateQueryHandler}
icon={<Disc3 size={14} />}
/>
</Tooltip>
</>
)}
</div>
)}
{!isExplorerOptionHidden && (
<div className="explorer-options">
<div
className="explorer-options"
style={{
background: extraData
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
: 'transparent',
}}
>
<div className="view-options">
<Select<string, { key: string; value: string }>
showSearch
@@ -809,23 +872,49 @@ function ExplorerOptions({
</Select>
<Button
shape="default"
className={cx(
'periscope-btn secondary',
isEditDeleteSupported ? '' : 'hidden',
)}
shape="round"
onClick={handleSaveViewModalToggle}
className={isEditDeleteSupported ? '' : 'hidden'}
disabled={viewsIsLoading || isRefetching}
icon={<Disc3 size={12} />}
icon={<Disc3 size={16} />}
>
Save this view
</Button>
</div>
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
{alertButton}
{dashboardButton}
</div>
<div className="actions">
{/* Hide the info icon for metrics explorer until we get the docs link */}
{!isMetricsExplorer && (
<Tooltip
title={
<div>
{infoIconText}
<Typography.Link href={infoIconLink} target="_blank">
{' '}
here
</Typography.Link>{' '}
</div>
}
>
<InfoCircleOutlined className="info-icon" />
</Tooltip>
)}
<Tooltip title="Hide">
<Button
disabled={disabled}
shape="circle"
onClick={hideToolbar}
icon={<PanelBottomClose size={16} />}
data-testid="hide-toolbar"
/>
</Tooltip>
</div>
</div>
)}
<ExplorerOptionsHideArea

View File

@@ -57,9 +57,7 @@ function FormAlertChannels({
return (
<>
<Typography.Title level={4} className="form-alert-channels-title">
{title}
</Typography.Title>
<Typography.Title level={3}>{title}</Typography.Title>
<Form initialValues={initialValue} layout="vertical" form={formInstance}>
<Form.Item label={t('field_channel_name')} labelAlign="left" name="name">
@@ -149,7 +147,7 @@ function FormAlertChannels({
</Button>
<Button
onClick={(): void => {
history.replace(ROUTES.ALL_CHANNELS);
history.replace(ROUTES.SETTINGS);
}}
>
{t('button_return')}

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