Compare commits

..

28 Commits

Author SHA1 Message Date
SagarRajput-7
edf9c6cc1f chore: base path setup and routing and navigation migrations 2026-04-14 19:03:20 +05:30
SagarRajput-7
db6da1784d feat: set React Router basename from <base href> tag 2026-04-14 19:03:20 +05:30
SagarRajput-7
07f412f748 test: add getBasePath() contract test; mark basePath export as internal 2026-04-14 19:03:20 +05:30
SagarRajput-7
0216fd61b8 feat: add getBasePath() utility reading <base href> from DOM 2026-04-14 19:03:20 +05:30
SagarRajput-7
52f097aee8 chore: public dir cleanup, remove duplicate assets since they are moved to src/assets 2026-04-14 16:56:55 +05:30
SagarRajput-7
bda93ee286 Merge branch 'main' into asset-consumption 2026-04-14 16:55:41 +05:30
SagarRajput-7
7edc737076 chore: addressed feedback comments 2026-04-14 16:54:56 +05:30
Tushar Vats
381966adcd fix: flaky integration tests (#10927) 2026-04-14 10:52:16 +00:00
SagarRajput-7
4fa306f94f chore: strengthen the lint rule and migration the gaps found 2026-04-14 16:11:21 +05:30
SagarRajput-7
c415b2a1d7 chore: fmt fix 2026-04-14 15:16:46 +05:30
SagarRajput-7
d513188511 chore: code refactor 2026-04-14 15:16:46 +05:30
SagarRajput-7
e7edc5da82 chore: asset migration - using the src/assets across the codebase, instead of the public dir 2026-04-14 15:16:46 +05:30
SagarRajput-7
dae7a43a52 chore: added eslint and stylelint for the asset migration task 2026-04-14 15:16:46 +05:30
SagarRajput-7
9fb373e36b chore: remove the snapshot update since no migration is done under this pr 2026-04-14 15:16:46 +05:30
SagarRajput-7
b01d4d8a74 chore: jest module resolution fix 2026-04-14 15:16:46 +05:30
Piyush Singariya
df9023c74c chore: upgrade collector version v0.144.3-rc.4 (#10921)
* fix: upgraded collector version

* fix: qbtoexpr tests

* fix: go sum

* chore: upgrade collector version v0.144.3-rc.4

* fix: tests

* ci: test fix
2026-04-14 08:57:28 +00:00
Nikhil Soni
10fc166e97 feat: add docs and agent skill info banner to ClickHouse query editor (#10713)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat: add clickhouse query doc and skill links

* fix: switch to using callout component for info

* chore: update banner message

* chore: move styles to a separate file and rename folder

to match conventions

* fix: rename folder in git as well
2026-04-14 06:17:13 +00:00
SagarRajput-7
22b137c92b chore: moved json to ts for the onboarding data sources (#10905)
* chore: moved json to ts for the onboarding data sources

* chore: migrated unsupported occurences in scss files

* chore: jest module resolution fix

* chore: remove the snapshot update since no migration is done under this pr
2026-04-14 05:54:49 +00:00
SagarRajput-7
0b20afc469 chore: duplicated all the assets required for migration from public dir to src/assets dir (#10904)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: duplicated all the assets required for migration from public dir to src/assets dir

* chore: jest module resolution fix

* chore: remove the snapshot update since no migration is done under this pr
2026-04-13 19:12:17 +00:00
swapnil-signoz
dce496d099 refactor: cloud integration modules implementation (#10718)
* feat: adding cloud integration type for refactor

* refactor: store interfaces to use local types and error

* feat: adding sql store implementation

* refactor: removing interface check

* feat: adding updated types for cloud integration

* refactor: using struct for map

* refactor: update cloud integration types and module interface

* fix: correct GetService signature and remove shadowed Data field

* feat: implement cloud integration store

* refactor: adding comments and removed wrong code

* refactor: streamlining types

* refactor: add comments for backward compatibility in PostableAgentCheckInRequest

* refactor: update Dashboard struct comments and remove unused fields

* refactor: split upsert store method

* feat: adding integration test

* refactor: clean up types

* refactor: renaming service type to service id

* refactor: using serviceID type

* feat: adding method for service id creation

* refactor: updating store methods

* refactor: clean up

* refactor: clean up

* refactor: review comments

* refactor: clean up

* feat: adding handlers

* fix: lint and ci issues

* fix: lint issues

* fix: update error code for service not found

* feat: adding handler skeleton

* chore: removing todo comment

* feat: adding frontend openapi schema

* feat: adding module implementation for create account

* fix: returning valid error instead of panic

* fix: module test

* refactor: simplify ingestion key retrieval logic

* feat: adding module implementation for AWS

* refactor: ci lint changes

* refactor: python formatting change

* fix: new storable account func was unsetting provider account id

* refactor: python lint changes

* refactor: adding validation on update account request

* refactor: reverting older tests and adding new tests

* chore: lint changes

* feat: using service account for API key

* refactor: renaming tests and cleanup

* refactor: removing dashboard overview images

* feat: adding service definition store

* chore: adding TODO comments

* feat: adding API for getting connection credentials

* feat: adding openapi spec for the endpoint

* feat: adding tests for credential API

* feat: adding cloud integration config

* refactor: updating test with new env variable for config

* refactor: moving few cloud provider interface methods to types

* refactor: review comments resolution

* refactor: lint changes

* refactor: code clean up

* refactor: removing email domain function

* refactor: review comments and clean up

* refactor: lint fixes

* refactor: review changes

- Added get connected account module method
- Fixed integration tests
- Removed cloud integration store as callback function's param

* refactor: changing wrong dashboard id for EKS definition
2026-04-13 15:16:26 +00:00
aniketio-ctrl
c15e3ca59e feat(billing): use meter api from zeus (#10903)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat(meter-api): use meter api from zeus

* feat(meter-api): bug fixes

* feat(meter-api): bug fixes

* feat(meter-api): bug fixes
2026-04-13 13:34:06 +00:00
Vikrant Gupta
9ec6da5e76 fix(authz): unauthenticated error code for deleted service account (#10878)
* fix(authz): populate correct error for deleted service account

* chore(authz): reduce the regex restrictions on service accounts

* chore(authz): reduce the regex restrictions on service accounts

* fix(authz): populate correct error for deleted service account

* fix(authz): populate correct error for deleted service account
2026-04-13 13:19:25 +00:00
Vikrant Gupta
3e241944e7 chore(sqlstore): added connection max lifetime support (#10913)
* chore(sqlstore): added connection max lifetime param support

* chore(sqlstore): fix test

* chore(sqlstore): default to 0 for conn_max_lifetime

* chore(sqlstore): rename the config

* chore(sqlstore): rename the config
2026-04-13 13:09:00 +00:00
SagarRajput-7
5251339a16 chore: config setup for asset migration, allow use of @ alias (#10901)
* chore: config setup for asset migration, allow use of @ alias

* chore: added modulewrapper for jests

* chore: jest module resolution fix

* chore: remove the snapshot update since no migration is done under this pr
2026-04-13 12:31:47 +00:00
Abhi kumar
702a16f80d chore: added test-ids to identify elements better in e2es (#10914) 2026-04-13 12:12:53 +00:00
swapnil-signoz
7279c5f770 feat: adding query params in cloud integration APIs (#10900)
Some checks failed
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
* feat: adding query params in cloud integration APIs

* refactor: create account HTTP status change from OK to CREATED
2026-04-10 09:20:35 +00:00
Nikhil Soni
e543776efc chore: send obfuscate query in the clickhouse query panel update (#10848)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: send query in the clickhouse query panel update

* chore: obfuscate query to avoid sending sensitive values
2026-04-09 14:15:10 +00:00
Pandey
621127b7fb feat(audit): wire auditor into DI graph and service lifecycle (#10891)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat(audit): wire auditor into DI graph and service lifecycle

Register the auditor in the factory service registry so it participates
in application lifecycle (start/stop/health). Community uses noopauditor,
enterprise uses otlphttpauditor with licensing gate. Pass the auditor
instance to the audit middleware instead of nil.

* feat(audit): use NamedMap provider pattern with config-driven selection

Switch from single-factory callback to NamedMap + factory.NewProviderFromNamedMap
so the config's Provider field selects the auditor implementation. Add
NewAuditorProviderFactories() with noop as the community default. Enterprise
extends the map with otlphttpauditor. Add auditor section to conf/example.yaml
and set default provider to "noop" in config.

* chore: move auditor config to end of example.yaml
2026-04-09 11:44:05 +00:00
176 changed files with 5370 additions and 821 deletions

View File

@@ -8,6 +8,7 @@ import (
"github.com/SigNoz/signoz/cmd"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/authz/openfgaauthz"
@@ -17,11 +18,15 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/gateway"
"github.com/SigNoz/signoz/pkg/gateway/noopgateway"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/queryparser"
@@ -93,9 +98,15 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return noopgateway.NewProviderFactory()
},
func(_ licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]] {
return signoz.NewAuditorProviderFactories()
},
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
return querier.NewHandler(ps, q, a)
},
func(_ sqlstore.SQLStore, _ global.Global, _ zeus.Zeus, _ gateway.Gateway, _ licensing.Licensing, _ serviceaccount.Module, _ cloudintegration.Config) (cloudintegration.Module, error) {
return implcloudintegration.NewModule(), nil
},
)
if err != nil {
logger.ErrorContext(ctx, "failed to create signoz", errors.Attr(err))

View File

@@ -8,6 +8,7 @@ import (
"github.com/spf13/cobra"
"github.com/SigNoz/signoz/cmd"
"github.com/SigNoz/signoz/ee/auditor/otlphttpauditor"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
@@ -16,6 +17,8 @@ import (
"github.com/SigNoz/signoz/ee/gateway/httpgateway"
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
"github.com/SigNoz/signoz/ee/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/ee/modules/cloudintegration/implcloudintegration/implcloudprovider"
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
eequerier "github.com/SigNoz/signoz/ee/querier"
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
@@ -24,15 +27,20 @@ import (
enterprisezeus "github.com/SigNoz/signoz/ee/zeus"
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/gateway"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
pkgcloudintegration "github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/signoz"
@@ -40,6 +48,7 @@ import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
"github.com/SigNoz/signoz/pkg/version"
"github.com/SigNoz/signoz/pkg/zeus"
)
@@ -125,7 +134,6 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return nil, err
}
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, dashboardModule), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)
@@ -133,12 +141,32 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return httpgateway.NewProviderFactory(licensing)
},
func(licensing licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]] {
factories := signoz.NewAuditorProviderFactories()
if err := factories.Add(otlphttpauditor.NewFactory(licensing, version.Info)); err != nil {
panic(err)
}
return factories
},
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
communityHandler := querier.NewHandler(ps, q, a)
return eequerier.NewHandler(ps, q, communityHandler)
},
)
func(sqlStore sqlstore.SQLStore, global global.Global, zeus zeus.Zeus, gateway gateway.Gateway, licensing licensing.Licensing, serviceAccount serviceaccount.Module, config cloudintegration.Config) (cloudintegration.Module, error) {
defStore := pkgcloudintegration.NewServiceDefinitionStore()
awsCloudProviderModule, err := implcloudprovider.NewAWSCloudProvider(defStore)
if err != nil {
return nil, err
}
azureCloudProviderModule := implcloudprovider.NewAzureCloudProvider()
cloudProvidersMap := map[cloudintegrationtypes.CloudProviderType]cloudintegration.CloudProviderModule{
cloudintegrationtypes.CloudProviderTypeAWS: awsCloudProviderModule,
cloudintegrationtypes.CloudProviderTypeAzure: azureCloudProviderModule,
}
return implcloudintegration.NewModule(pkgcloudintegration.NewStore(sqlStore), global, zeus, gateway, licensing, serviceAccount, cloudProvidersMap, config)
},
)
if err != nil {
logger.ErrorContext(ctx, "failed to create signoz", errors.Attr(err))
return err

View File

@@ -82,6 +82,9 @@ sqlstore:
provider: sqlite
# The maximum number of open connections to the database.
max_open_conns: 100
# The maximum amount of time a connection may be reused.
# If max_conn_lifetime == 0, connections are not closed due to a connection's age.
max_conn_lifetime: 0
sqlite:
# The path to the SQLite database file.
path: /var/lib/signoz/signoz.db
@@ -364,3 +367,41 @@ serviceaccount:
analytics:
# toggle service account analytics
enabled: true
##################### Auditor #####################
auditor:
# Specifies the auditor provider to use.
# noop: discards all audit events (community default).
# otlphttp: exports audit events via OTLP HTTP (enterprise).
provider: noop
# The async channel capacity for audit events. Events are dropped when full (fail-open).
buffer_size: 1000
# The maximum number of events per export batch.
batch_size: 100
# The maximum time between export flushes.
flush_interval: 1s
otlphttp:
# The target scheme://host:port/path of the OTLP HTTP endpoint.
endpoint: http://localhost:4318/v1/logs
# Whether to use HTTP instead of HTTPS.
insecure: false
# The maximum duration for an export attempt.
timeout: 10s
# Additional HTTP headers sent with every export request.
headers: {}
retry:
# Whether to retry on transient failures.
enabled: true
# The initial wait time before the first retry.
initial_interval: 5s
# The upper bound on backoff interval.
max_interval: 30s
# The total maximum time spent retrying.
max_elapsed_time: 60s
##################### Cloud Integration #####################
cloudintegration:
# cloud integration agent configuration
agent:
# The version of the cloud integration agent.
version: v0.0.8

View File

@@ -3309,7 +3309,7 @@ paths:
schema:
$ref: '#/components/schemas/CloudintegrationtypesPostableAccount'
responses:
"200":
"201":
content:
application/json:
schema:
@@ -3322,7 +3322,7 @@ paths:
- status
- data
type: object
description: OK
description: Created
"401":
content:
application/json:
@@ -3683,6 +3683,11 @@ paths:
provider
operationId: ListServicesMetadata
parameters:
- in: query
name: cloud_integration_id
required: false
schema:
type: string
- in: path
name: cloud_provider
required: true
@@ -3735,6 +3740,11 @@ paths:
description: This endpoint gets a service for the specified cloud provider
operationId: GetService
parameters:
- in: query
name: cloud_integration_id
required: false
schema:
type: string
- in: path
name: cloud_provider
required: true

View File

@@ -0,0 +1,132 @@
package implcloudprovider
import (
"context"
"fmt"
"net/url"
"sort"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
)
type awscloudprovider struct {
serviceDefinitions cloudintegrationtypes.ServiceDefinitionStore
}
func NewAWSCloudProvider(defStore cloudintegrationtypes.ServiceDefinitionStore) (cloudintegration.CloudProviderModule, error) {
return &awscloudprovider{serviceDefinitions: defStore}, nil
}
func (provider *awscloudprovider) GetConnectionArtifact(ctx context.Context, account *cloudintegrationtypes.Account, req *cloudintegrationtypes.GetConnectionArtifactRequest) (*cloudintegrationtypes.ConnectionArtifact, error) {
baseURL := fmt.Sprintf(cloudintegrationtypes.CloudFormationQuickCreateBaseURL.StringValue(), req.Config.Aws.DeploymentRegion)
u, _ := url.Parse(baseURL)
q := u.Query()
q.Set("region", req.Config.Aws.DeploymentRegion)
u.Fragment = "/stacks/quickcreate"
u.RawQuery = q.Encode()
q = u.Query()
q.Set("stackName", cloudintegrationtypes.AgentCloudFormationBaseStackName.StringValue())
q.Set("templateURL", fmt.Sprintf(cloudintegrationtypes.AgentCloudFormationTemplateS3Path.StringValue(), req.Config.AgentVersion))
q.Set("param_SigNozIntegrationAgentVersion", req.Config.AgentVersion)
q.Set("param_SigNozApiUrl", req.Credentials.SigNozAPIURL)
q.Set("param_SigNozApiKey", req.Credentials.SigNozAPIKey)
q.Set("param_SigNozAccountId", account.ID.StringValue())
q.Set("param_IngestionUrl", req.Credentials.IngestionURL)
q.Set("param_IngestionKey", req.Credentials.IngestionKey)
return &cloudintegrationtypes.ConnectionArtifact{
Aws: &cloudintegrationtypes.AWSConnectionArtifact{
ConnectionURL: u.String() + "?&" + q.Encode(), // this format is required by AWS
},
}, nil
}
func (provider *awscloudprovider) ListServiceDefinitions(ctx context.Context) ([]*cloudintegrationtypes.ServiceDefinition, error) {
return provider.serviceDefinitions.List(ctx, cloudintegrationtypes.CloudProviderTypeAWS)
}
func (provider *awscloudprovider) GetServiceDefinition(ctx context.Context, serviceID cloudintegrationtypes.ServiceID) (*cloudintegrationtypes.ServiceDefinition, error) {
serviceDef, err := provider.serviceDefinitions.Get(ctx, cloudintegrationtypes.CloudProviderTypeAWS, serviceID)
if err != nil {
return nil, err
}
// override cloud integration dashboard id
for index, dashboard := range serviceDef.Assets.Dashboards {
serviceDef.Assets.Dashboards[index].ID = cloudintegrationtypes.GetCloudIntegrationDashboardID(cloudintegrationtypes.CloudProviderTypeAWS, serviceID.StringValue(), dashboard.ID)
}
return serviceDef, nil
}
func (provider *awscloudprovider) BuildIntegrationConfig(
ctx context.Context,
account *cloudintegrationtypes.Account,
services []*cloudintegrationtypes.StorableCloudIntegrationService,
) (*cloudintegrationtypes.ProviderIntegrationConfig, error) {
// Sort services for deterministic output
sort.Slice(services, func(i, j int) bool {
return services[i].Type.StringValue() < services[j].Type.StringValue()
})
compiledMetrics := new(cloudintegrationtypes.AWSMetricsCollectionStrategy)
compiledLogs := new(cloudintegrationtypes.AWSLogsCollectionStrategy)
var compiledS3Buckets map[string][]string
for _, storedSvc := range services {
svcCfg, err := cloudintegrationtypes.NewServiceConfigFromJSON(cloudintegrationtypes.CloudProviderTypeAWS, storedSvc.Config)
if err != nil {
return nil, err
}
svcDef, err := provider.GetServiceDefinition(ctx, storedSvc.Type)
if err != nil {
return nil, err
}
strategy := svcDef.TelemetryCollectionStrategy.AWS
logsEnabled := svcCfg.IsLogsEnabled(cloudintegrationtypes.CloudProviderTypeAWS)
// S3Sync: logs come directly from configured S3 buckets, not CloudWatch subscriptions
if storedSvc.Type == cloudintegrationtypes.AWSServiceS3Sync {
if logsEnabled && svcCfg.AWS.Logs.S3Buckets != nil {
compiledS3Buckets = svcCfg.AWS.Logs.S3Buckets
}
// no need to go ahead as the code block specifically checks for the S3Sync service
continue
}
if logsEnabled && strategy.Logs != nil {
compiledLogs.Subscriptions = append(compiledLogs.Subscriptions, strategy.Logs.Subscriptions...)
}
metricsEnabled := svcCfg.IsMetricsEnabled(cloudintegrationtypes.CloudProviderTypeAWS)
if metricsEnabled && strategy.Metrics != nil {
compiledMetrics.StreamFilters = append(compiledMetrics.StreamFilters, strategy.Metrics.StreamFilters...)
}
}
collectionStrategy := new(cloudintegrationtypes.AWSTelemetryCollectionStrategy)
if len(compiledMetrics.StreamFilters) > 0 {
collectionStrategy.Metrics = compiledMetrics
}
if len(compiledLogs.Subscriptions) > 0 {
collectionStrategy.Logs = compiledLogs
}
if compiledS3Buckets != nil {
collectionStrategy.S3Buckets = compiledS3Buckets
}
return &cloudintegrationtypes.ProviderIntegrationConfig{
AWS: &cloudintegrationtypes.AWSIntegrationConfig{
EnabledRegions: account.Config.AWS.Regions,
TelemetryCollectionStrategy: collectionStrategy,
},
}, nil
}

View File

@@ -0,0 +1,34 @@
package implcloudprovider
import (
"context"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
)
type azurecloudprovider struct{}
func NewAzureCloudProvider() cloudintegration.CloudProviderModule {
return &azurecloudprovider{}
}
func (provider *azurecloudprovider) GetConnectionArtifact(ctx context.Context, account *cloudintegrationtypes.Account, req *cloudintegrationtypes.GetConnectionArtifactRequest) (*cloudintegrationtypes.ConnectionArtifact, error) {
panic("implement me")
}
func (provider *azurecloudprovider) ListServiceDefinitions(ctx context.Context) ([]*cloudintegrationtypes.ServiceDefinition, error) {
panic("implement me")
}
func (provider *azurecloudprovider) GetServiceDefinition(ctx context.Context, serviceID cloudintegrationtypes.ServiceID) (*cloudintegrationtypes.ServiceDefinition, error) {
panic("implement me")
}
func (provider *azurecloudprovider) BuildIntegrationConfig(
ctx context.Context,
account *cloudintegrationtypes.Account,
services []*cloudintegrationtypes.StorableCloudIntegrationService,
) (*cloudintegrationtypes.ProviderIntegrationConfig, error) {
panic("implement me")
}

View File

@@ -0,0 +1,540 @@
package implcloudintegration
import (
"context"
"fmt"
"sort"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/gateway"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/zeus"
)
type module struct {
store cloudintegrationtypes.Store
gateway gateway.Gateway
zeus zeus.Zeus
licensing licensing.Licensing
global global.Global
serviceAccount serviceaccount.Module
cloudProvidersMap map[cloudintegrationtypes.CloudProviderType]cloudintegration.CloudProviderModule
config cloudintegration.Config
}
func NewModule(
store cloudintegrationtypes.Store,
global global.Global,
zeus zeus.Zeus,
gateway gateway.Gateway,
licensing licensing.Licensing,
serviceAccount serviceaccount.Module,
cloudProvidersMap map[cloudintegrationtypes.CloudProviderType]cloudintegration.CloudProviderModule,
config cloudintegration.Config,
) (cloudintegration.Module, error) {
return &module{
store: store,
global: global,
zeus: zeus,
gateway: gateway,
licensing: licensing,
serviceAccount: serviceAccount,
cloudProvidersMap: cloudProvidersMap,
config: config,
}, nil
}
// GetConnectionCredentials returns credentials required to generate connection artifact. eg. apiKey, ingestionKey etc.
// It will return creds it can deduce and return empty value for others.
func (module *module) GetConnectionCredentials(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.Credentials, error) {
// get license to get the deployment details
license, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
// get deployment details from zeus, ignore error
respBytes, _ := module.zeus.GetDeployment(ctx, license.Key)
var signozAPIURL string
if len(respBytes) > 0 {
// parse deployment details, ignore error, if client is asking api url every time check for possible error
deployment, _ := zeustypes.NewGettableDeployment(respBytes)
if deployment != nil {
signozAPIURL, _ = cloudintegrationtypes.GetSigNozAPIURLFromDeployment(deployment)
}
}
// ignore error
apiKey, _ := module.getOrCreateAPIKey(ctx, orgID, provider)
// ignore error
ingestionKey, _ := module.getOrCreateIngestionKey(ctx, orgID, provider)
return cloudintegrationtypes.NewCredentials(
signozAPIURL,
apiKey,
module.global.GetConfig(ctx).IngestionURL,
ingestionKey,
), nil
}
func (module *module) CreateAccount(ctx context.Context, account *cloudintegrationtypes.Account) error {
_, err := module.licensing.GetActive(ctx, account.OrgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
storableCloudIntegration, err := cloudintegrationtypes.NewStorableCloudIntegration(account)
if err != nil {
return err
}
return module.store.CreateAccount(ctx, storableCloudIntegration)
}
func (module *module) GetConnectionArtifact(ctx context.Context, account *cloudintegrationtypes.Account, req *cloudintegrationtypes.GetConnectionArtifactRequest) (*cloudintegrationtypes.ConnectionArtifact, error) {
_, err := module.licensing.GetActive(ctx, account.OrgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
cloudProviderModule, err := module.getCloudProvider(account.Provider)
if err != nil {
return nil, err
}
req.Config.SetAgentVersion(module.config.Agent.Version)
return cloudProviderModule.GetConnectionArtifact(ctx, account, req)
}
func (module *module) GetAccount(ctx context.Context, orgID valuer.UUID, accountID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.Account, error) {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
storableAccount, err := module.store.GetAccountByID(ctx, orgID, accountID, provider)
if err != nil {
return nil, err
}
return cloudintegrationtypes.NewAccountFromStorable(storableAccount)
}
func (module *module) GetConnectedAccount(ctx context.Context, orgID, accountID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.Account, error) {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
storableAccount, err := module.store.GetConnectedAccount(ctx, orgID, accountID, provider)
if err != nil {
return nil, err
}
return cloudintegrationtypes.NewAccountFromStorable(storableAccount)
}
// ListAccounts return only agent connected accounts.
func (module *module) ListAccounts(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) ([]*cloudintegrationtypes.Account, error) {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
storableAccounts, err := module.store.ListConnectedAccounts(ctx, orgID, provider)
if err != nil {
return nil, err
}
return cloudintegrationtypes.NewAccountsFromStorables(storableAccounts)
}
func (module *module) AgentCheckIn(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, req *cloudintegrationtypes.AgentCheckInRequest) (*cloudintegrationtypes.AgentCheckInResponse, error) {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
connectedAccount, err := module.store.GetConnectedAccountByProviderAccountID(ctx, orgID, req.ProviderAccountID, provider)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
// If a different integration is already connected to this provider account ID, reject the check-in.
// Allow re-check-in from the same integration (e.g. agent restarting).
if connectedAccount != nil && connectedAccount.ID != req.CloudIntegrationID {
errMessage := fmt.Sprintf("provider account id %s is already connected to cloud integration id %s", req.ProviderAccountID, connectedAccount.ID)
return nil, errors.New(errors.TypeAlreadyExists, cloudintegrationtypes.ErrCodeCloudIntegrationAlreadyConnected, errMessage)
}
account, err := module.store.GetAccountByID(ctx, orgID, req.CloudIntegrationID, provider)
if err != nil {
return nil, err
}
// If account has been removed (disconnected), return a minimal response with empty integration config.
// The agent uses this response to clean up resources
if account.RemovedAt != nil {
return cloudintegrationtypes.NewAgentCheckInResponse(
req.ProviderAccountID,
account.ID.StringValue(),
new(cloudintegrationtypes.ProviderIntegrationConfig),
account.RemovedAt,
), nil
}
// update account with cloud provider account id and agent report (heartbeat)
account.Update(&req.ProviderAccountID, cloudintegrationtypes.NewAgentReport(req.Data))
err = module.store.UpdateAccount(ctx, account)
if err != nil {
return nil, err
}
// Get account as domain object for config access (enabled regions, etc.)
domainAccount, err := cloudintegrationtypes.NewAccountFromStorable(account)
if err != nil {
return nil, err
}
cloudProvider, err := module.getCloudProvider(provider)
if err != nil {
return nil, err
}
storedServices, err := module.store.ListServices(ctx, req.CloudIntegrationID)
if err != nil {
return nil, err
}
// Delegate integration config building entirely to the provider module
integrationConfig, err := cloudProvider.BuildIntegrationConfig(ctx, domainAccount, storedServices)
if err != nil {
return nil, err
}
return cloudintegrationtypes.NewAgentCheckInResponse(
req.ProviderAccountID,
account.ID.StringValue(),
integrationConfig,
account.RemovedAt,
), nil
}
func (module *module) UpdateAccount(ctx context.Context, account *cloudintegrationtypes.Account) error {
_, err := module.licensing.GetActive(ctx, account.OrgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
storableAccount, err := cloudintegrationtypes.NewStorableCloudIntegration(account)
if err != nil {
return err
}
return module.store.UpdateAccount(ctx, storableAccount)
}
func (module *module) DisconnectAccount(ctx context.Context, orgID valuer.UUID, accountID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) error {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
return module.store.RemoveAccount(ctx, orgID, accountID, provider)
}
func (module *module) ListServicesMetadata(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, integrationID valuer.UUID) ([]*cloudintegrationtypes.ServiceMetadata, error) {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
cloudProvider, err := module.getCloudProvider(provider)
if err != nil {
return nil, err
}
serviceDefinitions, err := cloudProvider.ListServiceDefinitions(ctx)
if err != nil {
return nil, err
}
enabledServiceIDs := map[string]bool{}
if !integrationID.IsZero() {
storedServices, err := module.store.ListServices(ctx, integrationID)
if err != nil {
return nil, err
}
for _, svc := range storedServices {
serviceConfig, err := cloudintegrationtypes.NewServiceConfigFromJSON(provider, svc.Config)
if err != nil {
return nil, err
}
if serviceConfig.IsServiceEnabled(provider) {
enabledServiceIDs[svc.Type.StringValue()] = true
}
}
}
resp := make([]*cloudintegrationtypes.ServiceMetadata, 0, len(serviceDefinitions))
for _, serviceDefinition := range serviceDefinitions {
resp = append(resp, cloudintegrationtypes.NewServiceMetadata(*serviceDefinition, enabledServiceIDs[serviceDefinition.ID]))
}
return resp, nil
}
func (module *module) GetService(ctx context.Context, orgID valuer.UUID, serviceID cloudintegrationtypes.ServiceID, provider cloudintegrationtypes.CloudProviderType, cloudIntegrationID valuer.UUID) (*cloudintegrationtypes.Service, error) {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
cloudProvider, err := module.getCloudProvider(provider)
if err != nil {
return nil, err
}
serviceDefinition, err := cloudProvider.GetServiceDefinition(ctx, serviceID)
if err != nil {
return nil, err
}
var integrationService *cloudintegrationtypes.CloudIntegrationService
if !cloudIntegrationID.IsZero() {
storedService, err := module.store.GetServiceByServiceID(ctx, cloudIntegrationID, serviceID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
if storedService != nil {
serviceConfig, err := cloudintegrationtypes.NewServiceConfigFromJSON(provider, storedService.Config)
if err != nil {
return nil, err
}
integrationService = cloudintegrationtypes.NewCloudIntegrationServiceFromStorable(storedService, serviceConfig)
}
}
return cloudintegrationtypes.NewService(*serviceDefinition, integrationService), nil
}
func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
cloudProvider, err := module.getCloudProvider(provider)
if err != nil {
return err
}
serviceDefinition, err := cloudProvider.GetServiceDefinition(ctx, service.Type)
if err != nil {
return err
}
configJSON, err := service.Config.ToJSON(provider, service.Type, &serviceDefinition.SupportedSignals)
if err != nil {
return err
}
return module.store.CreateService(ctx, cloudintegrationtypes.NewStorableCloudIntegrationService(service, string(configJSON)))
}
func (module *module) UpdateService(ctx context.Context, orgID valuer.UUID, integrationService *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
cloudProvider, err := module.getCloudProvider(provider)
if err != nil {
return err
}
serviceDefinition, err := cloudProvider.GetServiceDefinition(ctx, integrationService.Type)
if err != nil {
return err
}
configJSON, err := integrationService.Config.ToJSON(provider, integrationService.Type, &serviceDefinition.SupportedSignals)
if err != nil {
return err
}
storableService := cloudintegrationtypes.NewStorableCloudIntegrationService(integrationService, string(configJSON))
return module.store.UpdateService(ctx, storableService)
}
func (module *module) GetDashboardByID(ctx context.Context, orgID valuer.UUID, id string) (*dashboardtypes.Dashboard, error) {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
_, _, _, err = cloudintegrationtypes.ParseCloudIntegrationDashboardID(id)
if err != nil {
return nil, err
}
allDashboards, err := module.listDashboards(ctx, orgID)
if err != nil {
return nil, err
}
for _, d := range allDashboards {
if d.ID == id {
return d, nil
}
}
return nil, errors.New(errors.TypeNotFound, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "cloud integration dashboard not found")
}
func (module *module) ListDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
return module.listDashboards(ctx, orgID)
}
func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
stats := make(map[string]any)
// get connected accounts for AWS
awsAccountsCount, err := module.store.CountConnectedAccounts(ctx, orgID, cloudintegrationtypes.CloudProviderTypeAWS)
if err == nil {
stats["cloudintegration.aws.connectedaccounts.count"] = awsAccountsCount
}
// NOTE: not adding stats for services for now.
// TODO: add more cloud providers when supported
return stats, nil
}
func (module *module) getCloudProvider(provider cloudintegrationtypes.CloudProviderType) (cloudintegration.CloudProviderModule, error) {
if cloudProviderModule, ok := module.cloudProvidersMap[provider]; ok {
return cloudProviderModule, nil
}
return nil, errors.NewInvalidInputf(cloudintegrationtypes.ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
}
func (module *module) getOrCreateIngestionKey(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (string, error) {
keyName := cloudintegrationtypes.NewIngestionKeyName(provider)
result, err := module.gateway.SearchIngestionKeysByName(ctx, orgID, keyName, 1, 10)
if err != nil {
return "", errors.WrapInternalf(err, errors.CodeInternal, "couldn't search ingestion keys")
}
// ideally there should be only one key per cloud integration provider
if len(result.Keys) > 0 {
return result.Keys[0].Value, nil
}
createdIngestionKey, err := module.gateway.CreateIngestionKey(ctx, orgID, keyName, []string{"integration"}, time.Time{})
if err != nil {
return "", errors.WrapInternalf(err, errors.CodeInternal, "couldn't create ingestion key")
}
return createdIngestionKey.Value, nil
}
func (module *module) getOrCreateAPIKey(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (string, error) {
domain := module.serviceAccount.Config().Email.Domain
serviceAccount := serviceaccounttypes.NewServiceAccount("integration", domain, serviceaccounttypes.ServiceAccountStatusActive, orgID)
serviceAccount, err := module.serviceAccount.GetOrCreate(ctx, orgID, serviceAccount)
if err != nil {
return "", err
}
err = module.serviceAccount.SetRoleByName(ctx, orgID, serviceAccount.ID, authtypes.SigNozViewerRoleName)
if err != nil {
return "", err
}
factorAPIKey, err := serviceAccount.NewFactorAPIKey(provider.StringValue(), 0)
if err != nil {
return "", err
}
factorAPIKey, err = module.serviceAccount.GetOrCreateFactorAPIKey(ctx, factorAPIKey)
if err != nil {
return "", err
}
return factorAPIKey.Key, nil
}
func (module *module) listDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
var allDashboards []*dashboardtypes.Dashboard
for provider := range module.cloudProvidersMap {
cloudProvider, err := module.getCloudProvider(provider)
if err != nil {
return nil, err
}
connectedAccounts, err := module.store.ListConnectedAccounts(ctx, orgID, provider)
if err != nil {
return nil, err
}
for _, storableAccount := range connectedAccounts {
storedServices, err := module.store.ListServices(ctx, storableAccount.ID)
if err != nil {
return nil, err
}
for _, storedSvc := range storedServices {
serviceConfig, err := cloudintegrationtypes.NewServiceConfigFromJSON(provider, storedSvc.Config)
if err != nil || !serviceConfig.IsMetricsEnabled(provider) {
continue
}
svcDef, err := cloudProvider.GetServiceDefinition(ctx, storedSvc.Type)
if err != nil || svcDef == nil {
continue
}
dashboards := cloudintegrationtypes.GetDashboardsFromAssets(
storedSvc.Type.StringValue(),
orgID,
provider,
storableAccount.CreatedAt,
svcDef.Assets,
)
allDashboards = append(allDashboards, dashboards...)
}
}
}
sort.Slice(allDashboards, func(i, j int) bool {
return allDashboards[i].ID < allDashboards[j].ID
})
return allDashboards, nil
}

View File

@@ -7,6 +7,10 @@ import (
"github.com/SigNoz/signoz/ee/query-service/constants"
"github.com/SigNoz/signoz/ee/query-service/model"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type DayWiseBreakdown struct {
@@ -45,15 +49,17 @@ type details struct {
BillTotal float64 `json:"billTotal"`
}
type billingData struct {
BillingPeriodStart int64 `json:"billingPeriodStart"`
BillingPeriodEnd int64 `json:"billingPeriodEnd"`
Details details `json:"details"`
Discount float64 `json:"discount"`
SubscriptionStatus string `json:"subscriptionStatus"`
}
type billingDetails struct {
Status string `json:"status"`
Data struct {
BillingPeriodStart int64 `json:"billingPeriodStart"`
BillingPeriodEnd int64 `json:"billingPeriodEnd"`
Details details `json:"details"`
Discount float64 `json:"discount"`
SubscriptionStatus string `json:"subscriptionStatus"`
} `json:"data"`
Status string `json:"status"`
Data billingData `json:"data"`
}
func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
@@ -64,6 +70,33 @@ func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
return
}
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
useZeus := ah.Signoz.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureGetMetersFromZeus, evalCtx)
if useZeus {
data, err := ah.Signoz.Zeus.GetMeters(r.Context(), licenseKey)
if err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
var billing billingData
if err := json.Unmarshal(data, &billing); err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
ah.Respond(w, billing)
return
}
billingURL := fmt.Sprintf("%s/usage?licenseKey=%s", constants.LicenseSignozIo, licenseKey)
hClient := &http.Client{}
@@ -79,13 +112,11 @@ func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
return
}
// decode response body
var billingResponse billingDetails
if err := json.NewDecoder(billingResp.Body).Decode(&billingResponse); err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
// TODO(srikanthccv):Fetch the current day usage and add it to the response
ah.Respond(w, billingResponse.Data)
}

View File

@@ -227,7 +227,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
s.config.APIServer.Timeout.Default,
s.config.APIServer.Timeout.Max,
).Wrap)
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, nil).Wrap)
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, s.signoz.Auditor).Wrap)
r.Use(middleware.NewComment().Wrap)
apiHandler.RegisterRoutes(r, am)

View File

@@ -54,6 +54,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
// Set the maximum number of open connections
pgConfig.MaxConns = int32(config.Connection.MaxOpenConns)
pgConfig.MaxConnLifetime = config.Connection.MaxConnLifetime
// Use pgxpool to create a connection pool
pool, err := pgxpool.NewWithConfig(ctx, pgConfig)

View File

@@ -109,6 +109,21 @@ func (provider *Provider) GetDeployment(ctx context.Context, key string) ([]byte
return []byte(gjson.GetBytes(response, "data").String()), nil
}
func (provider *Provider) GetMeters(ctx context.Context, key string) ([]byte, error) {
response, err := provider.do(
ctx,
provider.config.URL.JoinPath("/v1/meters"),
http.MethodGet,
key,
nil,
)
if err != nil {
return nil, err
}
return []byte(gjson.GetBytes(response, "data").String()), nil
}
func (provider *Provider) PutMeters(ctx context.Context, key string, data []byte) error {
_, err := provider.do(
ctx,

View File

@@ -1,5 +1,7 @@
node_modules
build
eslint-rules/
stylelint-rules/
*.typegen.ts
i18-generate-hash.js
src/parser/TraceOperatorParser/**

View File

@@ -1,3 +1,8 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const rulesDirPlugin = require('eslint-plugin-rulesdir');
rulesDirPlugin.RULES_DIR = path.join(__dirname, 'eslint-rules');
/**
* ESLint Configuration for SigNoz Frontend
*/
@@ -32,6 +37,7 @@ module.exports = {
sourceType: 'module',
},
plugins: [
'rulesdir', // Local custom rules
'react', // React-specific rules
'@typescript-eslint', // TypeScript linting
'simple-import-sort', // Auto-sort imports
@@ -56,6 +62,9 @@ module.exports = {
},
},
rules: {
// Asset migration — base-path safety
'rulesdir/no-unsupported-asset-pattern': 'error',
// Code quality rules
'prefer-const': 'error', // Enforces const for variables never reassigned
'no-var': 'error', // Disallows var, enforces let/const
@@ -202,6 +211,23 @@ module.exports = {
message:
'Avoid calling .getState() directly. Export a standalone action from the store instead.',
},
{
selector:
"CallExpression[callee.object.name='window'][callee.property.name='open']",
message:
'Do not call window.open() directly. ' +
"Use openInNewTab() from 'utils/navigation' for internal SigNoz paths. " +
"For intentional external URLs, use openExternalLink() from 'utils/navigation'. " +
'For unavoidable direct calls, add // eslint-disable-next-line with a reason.',
},
{
selector:
"AssignmentExpression[left.object.name='window'][left.property.name='href']",
message:
'Do not assign window.location.href for internal navigation. ' +
"Use history.push() or history.replace() from 'lib/history'. " +
'For external redirects (SSO, logout URLs), add // eslint-disable-next-line with a reason.',
},
],
},
overrides: [
@@ -254,5 +280,14 @@ module.exports = {
'no-restricted-syntax': 'off',
},
},
{
// navigation.ts and useSafeNavigate.ts are the canonical implementations that call
// window.open after computing a base-path-aware href. They are the only places
// allowed to call window.open directly.
files: ['src/utils/navigation.ts', 'src/hooks/useSafeNavigate.ts'],
rules: {
'no-restricted-syntax': 'off',
},
},
],
};

View File

@@ -0,0 +1,9 @@
const path = require('path');
module.exports = {
plugins: [path.join(__dirname, 'stylelint-rules/no-unsupported-asset-url.js')],
customSyntax: 'postcss-scss',
rules: {
'local/no-unsupported-asset-url': true,
},
};

View File

@@ -0,0 +1,268 @@
'use strict';
const {
hasAssetExtension,
containsAssetExtension,
extractUrlPath,
isAbsolutePath,
isPublicRelative,
isRelativePublicDir,
isValidAssetImport,
isExternalUrl,
} = require('./shared/asset-patterns');
const PUBLIC_DIR_SEGMENTS = ['/Icons/', '/Images/', '/Logos/', '/svgs/'];
function collectBinaryStringParts(node) {
if (node.type === 'Literal' && typeof node.value === 'string')
return [node.value];
if (node.type === 'BinaryExpression' && node.operator === '+') {
return [
...collectBinaryStringParts(node.left),
...collectBinaryStringParts(node.right),
];
}
if (node.type === 'TemplateLiteral') {
return node.quasis.map((q) => q.value.raw);
}
return [null];
}
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'Disallow Vite-unsafe asset reference patterns that break runtime base-path deployments',
category: 'Asset Migration',
recommended: true,
},
schema: [],
messages: {
absoluteString:
'Absolute asset path "{{ value }}" is not base-path-safe. ' +
"Use an ES import instead: import fooUrl from '@/assets/...' and reference the variable.",
templateLiteral:
'Dynamic asset path with absolute prefix is not base-path-safe. ' +
"Use new URL('./asset.svg', import.meta.url).href for dynamic asset paths.",
absoluteImport:
'Asset imported via absolute path is not supported. ' +
"Use import fooUrl from '@/assets/...' instead.",
publicImport:
"Assets in public/ bypass Vite's module pipeline — their URLs are not base-path-aware and will break when the app is served from a sub-path (e.g. /app/). " +
"Use an ES import instead: import fooUrl from '@/assets/...' so Vite injects the correct base path.",
relativePublicString:
'Relative public-dir path "{{ value }}" is not base-path-safe. ' +
"Use an ES import instead: import fooUrl from '@/assets/...' and reference the variable.",
invalidAssetImport:
"Asset '{{ src }}' must be imported from src/assets/ using either '@/assets/...' " +
'or a relative path into src/assets/.',
},
},
create(context) {
return {
Literal(node) {
if (node.parent && node.parent.type === 'ImportDeclaration') {
return;
}
const value = node.value;
if (typeof value !== 'string') return;
if (isExternalUrl(value)) return;
if (isAbsolutePath(value) && hasAssetExtension(value)) {
context.report({
node,
messageId: 'absoluteString',
data: { value },
});
return;
}
if (isRelativePublicDir(value) && hasAssetExtension(value)) {
context.report({
node,
messageId: 'relativePublicString',
data: { value },
});
return;
}
const urlPath = extractUrlPath(value);
if (urlPath && isExternalUrl(urlPath)) return;
if (urlPath && isAbsolutePath(urlPath) && hasAssetExtension(urlPath)) {
context.report({
node,
messageId: 'absoluteString',
data: { value: urlPath },
});
return;
}
if (urlPath && isRelativePublicDir(urlPath) && hasAssetExtension(urlPath)) {
context.report({
node,
messageId: 'relativePublicString',
data: { value: urlPath },
});
}
},
TemplateLiteral(node) {
const quasis = node.quasis;
if (!quasis || quasis.length === 0) return;
const firstQuasi = quasis[0].value.raw;
if (isExternalUrl(firstQuasi)) return;
const hasAssetExt = quasis.some((q) => containsAssetExtension(q.value.raw));
if (isAbsolutePath(firstQuasi) && hasAssetExt) {
context.report({
node,
messageId: 'templateLiteral',
});
return;
}
if (isRelativePublicDir(firstQuasi) && hasAssetExt) {
context.report({
node,
messageId: 'relativePublicString',
data: { value: firstQuasi },
});
return;
}
// Expression-first template with known public-dir segment: `${base}/Icons/foo.svg`
const hasPublicSegment = quasis.some((q) =>
PUBLIC_DIR_SEGMENTS.some((seg) => q.value.raw.includes(seg)),
);
if (hasPublicSegment && hasAssetExt) {
context.report({
node,
messageId: 'templateLiteral',
});
return;
}
if (quasis.length === 1) {
const urlPath = extractUrlPath(firstQuasi);
if (urlPath && isExternalUrl(urlPath)) return;
if (urlPath && isAbsolutePath(urlPath) && hasAssetExtension(urlPath)) {
context.report({
node,
messageId: 'templateLiteral',
});
return;
}
if (
urlPath &&
isRelativePublicDir(urlPath) &&
hasAssetExtension(urlPath)
) {
context.report({
node,
messageId: 'relativePublicString',
data: { value: urlPath },
});
}
return;
}
if (firstQuasi.includes('url(') && hasAssetExt) {
const urlMatch = firstQuasi.match(/^url\(\s*['"]?\//);
if (urlMatch) {
context.report({
node,
messageId: 'templateLiteral',
});
}
}
},
// String concatenation: "/Icons/" + name + ".svg" or "Icons/" + name + ".svg"
BinaryExpression(node) {
if (node.operator !== '+') return;
const parts = collectBinaryStringParts(node);
const prefixParts = [];
for (const part of parts) {
if (part === null) break;
prefixParts.push(part);
}
const staticPrefix = prefixParts.join('');
if (isExternalUrl(staticPrefix)) return;
const hasExt = parts.some(
(part) => part !== null && containsAssetExtension(part),
);
if (isAbsolutePath(staticPrefix) && hasExt) {
context.report({
node,
messageId: 'templateLiteral',
});
return;
}
if (isRelativePublicDir(staticPrefix) && hasExt) {
context.report({
node,
messageId: 'relativePublicString',
data: { value: staticPrefix },
});
}
},
ImportDeclaration(node) {
const src = node.source.value;
if (typeof src !== 'string') return;
if (!hasAssetExtension(src)) return;
if (isAbsolutePath(src)) {
context.report({ node, messageId: 'absoluteImport' });
return;
}
if (isPublicRelative(src)) {
context.report({ node, messageId: 'publicImport' });
return;
}
if (!isValidAssetImport(src)) {
context.report({
node,
messageId: 'invalidAssetImport',
data: { src },
});
}
},
ImportExpression(node) {
const src = node.source;
if (!src || src.type !== 'Literal' || typeof src.value !== 'string') return;
const value = src.value;
if (!hasAssetExtension(value)) return;
if (isAbsolutePath(value)) {
context.report({ node, messageId: 'absoluteImport' });
return;
}
if (isPublicRelative(value)) {
context.report({ node, messageId: 'publicImport' });
return;
}
if (!isValidAssetImport(value)) {
context.report({
node,
messageId: 'invalidAssetImport',
data: { src: value },
});
}
},
};
},
};

View File

@@ -0,0 +1,3 @@
{
"type": "commonjs"
}

View File

@@ -0,0 +1,114 @@
'use strict';
const ASSET_EXTENSIONS = ['.svg', '.png', '.webp', '.jpg', '.jpeg', '.gif'];
/**
* Returns true if the string ends with an asset extension.
* e.g. "/Icons/foo.svg" → true, "/Icons/foo.svg.bak" → false
*/
function hasAssetExtension(str) {
if (typeof str !== 'string') return false;
return ASSET_EXTENSIONS.some((ext) => str.endsWith(ext));
}
// Like hasAssetExtension but also matches mid-string with boundary check,
// e.g. "/foo.svg?v=1" → true, "/icons.svg-dir/" → true (- is non-alphanumeric boundary)
function containsAssetExtension(str) {
if (typeof str !== 'string') return false;
return ASSET_EXTENSIONS.some((ext) => {
const idx = str.indexOf(ext);
if (idx === -1) return false;
const afterIdx = idx + ext.length;
// Broad boundary (any non-alphanumeric) is intentional — the real guard against
// false positives is the upstream conditions (isAbsolutePath, isRelativePublicDir, etc.)
// that must pass before this is reached. "/icons.svg-dir/" → true (- is a boundary).
return afterIdx >= str.length || /[^a-zA-Z0-9]/.test(str[afterIdx]);
});
}
/**
* Extracts the asset path from a CSS url() wrapper.
* Handles single quotes, double quotes, unquoted, and whitespace variations.
* e.g.
* "url('/Icons/foo.svg')" → "/Icons/foo.svg"
* "url( '../assets/bg.png' )" → "../assets/bg.png"
* "url(/Icons/foo.svg)" → "/Icons/foo.svg"
* Returns null if the string is not a url() wrapper.
*/
function extractUrlPath(str) {
if (typeof str !== 'string') return null;
// Match url( [whitespace] [quote?] path [quote?] [whitespace] )
// Capture group: [^'")\s]+ matches path until quote, closing paren, or whitespace
const match = str.match(/^url\(\s*['"]?([^'")\s]+)['"]?\s*\)$/);
return match ? match[1] : null;
}
/**
* Returns true if the string is an absolute path (starts with /).
* Absolute paths in url() bypass <base href> and fail under any URL prefix.
*/
function isAbsolutePath(str) {
if (typeof str !== 'string') return false;
return str.startsWith('/') && !str.startsWith('//');
}
/**
* Returns true if the path imports from the public/ directory.
* Relative imports into public/ cause asset duplication in dist/.
*/
function isPublicRelative(str) {
if (typeof str !== 'string') return false;
return str.includes('/public/') || str.startsWith('public/');
}
/**
* Returns true if the string is a relative reference into a known public-dir folder.
* e.g. "Icons/foo.svg", `Logos/aws-dark.svg`, "Images/bg.png"
* These bypass Vite's module pipeline even without a leading slash.
*/
const PUBLIC_DIR_SEGMENTS = ['Icons/', 'Images/', 'Logos/', 'svgs/'];
function isRelativePublicDir(str) {
if (typeof str !== 'string') return false;
return PUBLIC_DIR_SEGMENTS.some((seg) => str.startsWith(seg));
}
/**
* Returns true if an asset import path is valid (goes through Vite's module pipeline).
* Valid: @/assets/..., any relative path containing /assets/, or node_modules packages.
* Invalid: absolute paths, public/ dir, or relative paths outside src/assets/.
*/
function isValidAssetImport(str) {
if (typeof str !== 'string') return false;
if (str.startsWith('@/assets/')) return true;
if (str.includes('/assets/')) return true;
// Not starting with . or / means it's a node_modules package — always valid
if (!str.startsWith('.') && !str.startsWith('/')) return true;
return false;
}
/**
* Returns true if the string is an external URL.
* Used to avoid false positives on CDN/API URLs with asset extensions.
*/
function isExternalUrl(str) {
if (typeof str !== 'string') return false;
return (
str.startsWith('http://') ||
str.startsWith('https://') ||
str.startsWith('//')
);
}
module.exports = {
ASSET_EXTENSIONS,
PUBLIC_DIR_SEGMENTS,
hasAssetExtension,
containsAssetExtension,
extractUrlPath,
isAbsolutePath,
isPublicRelative,
isRelativePublicDir,
isValidAssetImport,
isExternalUrl,
};

View File

@@ -6,3 +6,6 @@ VITE_APPCUES_APP_ID="appcess-app-id"
VITE_PYLON_IDENTITY_SECRET="pylon-identity-secret"
CI="1"
# Uncomment to test sub-path deployment locally (e.g. app served at /signoz/).
# VITE_BASE_PATH="/signoz/"

View File

@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="<%- BASE_PATH %>" />
<meta charset="utf-8" />
<meta
http-equiv="Cache-Control"
@@ -59,7 +60,7 @@
<meta data-react-helmet="true" name="docusaurus_locale" content="en" />
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
<meta name="robots" content="noindex" />
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
<link data-react-helmet="true" rel="shortcut icon" href="./favicon.ico" />
</head>
<body data-theme="default">
<noscript>You need to enable JavaScript to run this app.</noscript>
@@ -113,7 +114,7 @@
})(document, 'script');
}
</script>
<link rel="stylesheet" href="/css/uPlot.min.css" />
<link rel="stylesheet" href="./css/uPlot.min.css" />
<script type="module" src="./src/index.tsx"></script>
</body>
</html>

View File

@@ -20,6 +20,7 @@ const config: Config.InitialOptions = {
'^@signozhq/resizable$': '<rootDir>/__mocks__/resizableMock.tsx',
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^hooks/useSafeNavigate\\.impl$': '<rootDir>/src/hooks/useSafeNavigate.ts',
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^constants/env$': '<rootDir>/__mocks__/env.ts',
'^src/constants/env$': '<rootDir>/__mocks__/env.ts',

View File

@@ -10,9 +10,10 @@
"preview": "vite preview",
"prettify": "prettier --write .",
"fmt": "prettier --check .",
"lint": "eslint ./src",
"lint": "eslint ./src && stylelint \"src/**/*.scss\"",
"lint:generated": "eslint ./src/api/generated --fix",
"lint:fix": "eslint ./src --fix",
"lint:styles": "stylelint \"src/**/*.scss\"",
"jest": "jest",
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
@@ -229,6 +230,7 @@
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-rulesdir": "0.2.2",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-sonarjs": "^0.12.0",
"husky": "^7.0.4",
@@ -244,6 +246,7 @@
"orval": "7.18.0",
"portfinder-sync": "^0.0.2",
"postcss": "8.5.6",
"postcss-scss": "4.0.9",
"prettier": "2.2.1",
"prop-types": "15.8.1",
"react-hooks-testing-library": "0.6.0",
@@ -251,6 +254,8 @@
"redux-mock-store": "1.5.4",
"sass": "1.97.3",
"sharp": "0.34.5",
"stylelint": "17.7.0",
"stylelint-scss": "7.0.0",
"svgo": "4.0.0",
"ts-api-utils": "2.4.0",
"ts-jest": "29.4.6",

View File

@@ -28,7 +28,7 @@ import type {
CloudintegrationtypesPostableAgentCheckInDTO,
CloudintegrationtypesUpdatableAccountDTO,
CloudintegrationtypesUpdatableServiceDTO,
CreateAccount200,
CreateAccount201,
CreateAccountPathParameters,
DisconnectAccountPathParameters,
GetAccount200,
@@ -36,10 +36,12 @@ import type {
GetConnectionCredentials200,
GetConnectionCredentialsPathParameters,
GetService200,
GetServiceParams,
GetServicePathParameters,
ListAccounts200,
ListAccountsPathParameters,
ListServicesMetadata200,
ListServicesMetadataParams,
ListServicesMetadataPathParameters,
RenderErrorResponseDTO,
UpdateAccountPathParameters,
@@ -260,7 +262,7 @@ export const createAccount = (
cloudintegrationtypesPostableAccountDTO: BodyType<CloudintegrationtypesPostableAccountDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateAccount200>({
return GeneratedAPIInstance<CreateAccount201>({
url: `/api/v1/cloud_integrations/${cloudProvider}/accounts`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -940,19 +942,25 @@ export const invalidateGetConnectionCredentials = async (
*/
export const listServicesMetadata = (
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListServicesMetadata200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/services`,
method: 'GET',
params,
signal,
});
};
export const getListServicesMetadataQueryKey = ({
cloudProvider,
}: ListServicesMetadataPathParameters) => {
return [`/api/v1/cloud_integrations/${cloudProvider}/services`] as const;
export const getListServicesMetadataQueryKey = (
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
) => {
return [
`/api/v1/cloud_integrations/${cloudProvider}/services`,
...(params ? [params] : []),
] as const;
};
export const getListServicesMetadataQueryOptions = <
@@ -960,6 +968,7 @@ export const getListServicesMetadataQueryOptions = <
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listServicesMetadata>>,
@@ -971,11 +980,12 @@ export const getListServicesMetadataQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getListServicesMetadataQueryKey({ cloudProvider });
queryOptions?.queryKey ??
getListServicesMetadataQueryKey({ cloudProvider }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listServicesMetadata>>
> = ({ signal }) => listServicesMetadata({ cloudProvider }, signal);
> = ({ signal }) => listServicesMetadata({ cloudProvider }, params, signal);
return {
queryKey,
@@ -1003,6 +1013,7 @@ export function useListServicesMetadata<
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listServicesMetadata>>,
@@ -1013,6 +1024,7 @@ export function useListServicesMetadata<
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListServicesMetadataQueryOptions(
{ cloudProvider },
params,
options,
);
@@ -1031,10 +1043,11 @@ export function useListServicesMetadata<
export const invalidateListServicesMetadata = async (
queryClient: QueryClient,
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListServicesMetadataQueryKey({ cloudProvider }) },
{ queryKey: getListServicesMetadataQueryKey({ cloudProvider }, params) },
options,
);
@@ -1047,21 +1060,24 @@ export const invalidateListServicesMetadata = async (
*/
export const getService = (
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetService200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
method: 'GET',
params,
signal,
});
};
export const getGetServiceQueryKey = ({
cloudProvider,
serviceId,
}: GetServicePathParameters) => {
export const getGetServiceQueryKey = (
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
) => {
return [
`/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
...(params ? [params] : []),
] as const;
};
@@ -1070,6 +1086,7 @@ export const getGetServiceQueryOptions = <
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getService>>,
@@ -1081,11 +1098,12 @@ export const getGetServiceQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetServiceQueryKey({ cloudProvider, serviceId });
queryOptions?.queryKey ??
getGetServiceQueryKey({ cloudProvider, serviceId }, params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getService>>> = ({
signal,
}) => getService({ cloudProvider, serviceId }, signal);
}) => getService({ cloudProvider, serviceId }, params, signal);
return {
queryKey,
@@ -1111,6 +1129,7 @@ export function useGetService<
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getService>>,
@@ -1121,6 +1140,7 @@ export function useGetService<
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetServiceQueryOptions(
{ cloudProvider, serviceId },
params,
options,
);
@@ -1139,10 +1159,11 @@ export function useGetService<
export const invalidateGetService = async (
queryClient: QueryClient,
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetServiceQueryKey({ cloudProvider, serviceId }) },
{ queryKey: getGetServiceQueryKey({ cloudProvider, serviceId }, params) },
options,
);

View File

@@ -3589,7 +3589,7 @@ export type ListAccounts200 = {
export type CreateAccountPathParameters = {
cloudProvider: string;
};
export type CreateAccount200 = {
export type CreateAccount201 = {
data: CloudintegrationtypesGettableAccountWithConnectionArtifactDTO;
/**
* @type string
@@ -3647,6 +3647,14 @@ export type GetConnectionCredentials200 = {
export type ListServicesMetadataPathParameters = {
cloudProvider: string;
};
export type ListServicesMetadataParams = {
/**
* @type string
* @description undefined
*/
cloud_integration_id?: string;
};
export type ListServicesMetadata200 = {
data: CloudintegrationtypesGettableServicesMetadataDTO;
/**
@@ -3659,6 +3667,14 @@ export type GetServicePathParameters = {
cloudProvider: string;
serviceId: string;
};
export type GetServiceParams = {
/**
* @type string
* @description undefined
*/
cloud_integration_id?: string;
};
export type GetService200 = {
data: CloudintegrationtypesServiceDTO;
/**

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -47,7 +47,7 @@ describe('AppLoading', () => {
// Check for brand logo
const logo = screen.getByAltText(SIGNOZ_TEXT);
expect(logo).toBeInTheDocument();
expect(logo).toHaveAttribute('src');
expect(logo).toHaveAttribute('src', 'test-file-stub');
// Check for brand title
const title = screen.getByText(SIGNOZ_TEXT);

View File

@@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { Button } from '@signozhq/button';
import { LifeBuoy } from 'lucide-react';
import { openExternalLink } from 'utils/navigation';
import signozBrandLogoUrl from '@/assets/Logos/signoz-brand-logo.svg';
@@ -8,7 +9,7 @@ import './AuthHeader.styles.scss';
function AuthHeader(): JSX.Element {
const handleGetHelp = useCallback((): void => {
window.open('https://signoz.io/support/', '_blank');
openExternalLink('https://signoz.io/support/');
}, []);
return (

View File

@@ -7,11 +7,13 @@ import ROUTES from 'constants/routes';
import useUpdatedQuery from 'container/GridCardLayout/useResolveQuery';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { AppState } from 'store/reducers';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
export interface NavigateToExplorerProps {
filters: TagFilterItem[];
@@ -133,7 +135,11 @@ export function useNavigateToExplorer(): (
QueryParams.compositeQuery
}=${JSONCompositeQuery}`;
window.open(newExplorerPath, sameTab ? '_self' : '_blank');
if (sameTab) {
history.push(newExplorerPath);
} else {
openInNewTab(newExplorerPath);
}
},
[
prepareQuery,

View File

@@ -11,6 +11,7 @@ import { ChevronsDown, ScrollText } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { ChangelogSchema } from 'types/api/changelog/getChangelogByVersion';
import { UserPreference } from 'types/api/preferences/preference';
import { openExternalLink } from 'utils/navigation';
import ChangelogRenderer from './components/ChangelogRenderer';
@@ -86,11 +87,7 @@ function ChangelogModal({ changelog, onClose }: Props): JSX.Element {
}, [checkScroll]);
const onClickUpdateWorkspace = (): void => {
window.open(
'https://signoz.io/upgrade-path',
'_blank',
'noopener,noreferrer',
);
openExternalLink('https://signoz.io/upgrade-path');
};
const onClickScrollForMore = (): void => {

View File

@@ -34,7 +34,7 @@ const mockChangelog: ChangelogSchema = {
id: 1,
documentId: 'doc1',
ext: '.webp',
url: '/uploads/feature1.webp',
url: '/uploads/feature1.webp', // eslint-disable-line rulesdir/no-unsupported-asset-pattern
mime: 'image/webp',
alternativeText: null,
},

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import { VIEW_TYPES, VIEWS } from './constants';
@@ -330,10 +331,7 @@ function HostMetricsDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -352,10 +350,7 @@ function HostMetricsDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -1,6 +1,7 @@
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { ArrowUpRight } from 'lucide-react';
import { openExternalLink } from 'utils/navigation';
import './LearnMore.styles.scss';
@@ -14,7 +15,7 @@ function LearnMore({ text, url, onClick }: LearnMoreProps): JSX.Element {
const handleClick = (): void => {
onClick?.();
if (url) {
window.open(url, '_blank');
openExternalLink(url);
}
};
return (

View File

@@ -20,6 +20,7 @@ import {
KAFKA_SETUP_DOC_LINK,
MessagingQueueHealthCheckService,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { openExternalLink } from 'utils/navigation';
import { v4 as uuid } from 'uuid';
import './MessagingQueueHealthCheck.styles.scss';
@@ -76,7 +77,7 @@ function ErrorTitleAndKey({
if (isCloudUserVal && !!link) {
history.push(link);
} else {
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
openExternalLink(KAFKA_SETUP_DOC_LINK);
}
};
return {

View File

@@ -150,6 +150,7 @@ export const MetricsSelect = memo(function MetricsSelect({
options={SOURCE_OPTIONS}
value={source}
defaultValue="metrics"
data-testid={`metrics-source-selector-${index}`}
onChange={handleSignalSourceChange}
/>
)}
@@ -157,6 +158,7 @@ export const MetricsSelect = memo(function MetricsSelect({
<MetricNameSelector
onChange={handleChangeAggregatorAttribute}
query={query}
data-testid={`metric-name-selector-${index}`}
signalSource={signalSource || ''}
/>
</div>

View File

@@ -2,6 +2,7 @@ import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import EmptyQuickFilterIcon from 'assets/CustomIcons/EmptyQuickFilterIcon';
import { ArrowUpRight } from 'lucide-react';
import { openExternalLink } from 'utils/navigation';
const QUICK_FILTER_DOC_PATHS: Record<string, string> = {
severity_text: 'severity-text',
@@ -22,9 +23,8 @@ function LogsQuickFilterEmptyState({
const handleLearnMoreClick = (): void => {
const section = QUICK_FILTER_DOC_PATHS[attributeKey];
window.open(
openExternalLink(
`https://signoz.io/docs/logs-management/features/logs-quick-filters#${section}`,
'_blank',
);
};
return (

View File

@@ -9,6 +9,7 @@ import {
} from 'container/ApiMonitoring/utils';
import { UnfoldVertical } from 'lucide-react';
import { SuccessResponse } from 'types/api';
import { openInNewTab } from 'utils/navigation';
import emptyStateUrl from '@/assets/Icons/emptyState.svg';
@@ -107,7 +108,7 @@ function DependentServices({
urlQuery.set(QueryParams.startTime, timeRange.startTime.toString());
urlQuery.set(QueryParams.endTime, timeRange.endTime.toString());
url.search = urlQuery.toString();
window.open(url.toString(), '_blank');
openInNewTab(`${url.pathname}${url.search}`);
},
className: 'clickable-row',
})}

View File

@@ -1,6 +1,6 @@
import { useIsDarkMode } from 'hooks/useDarkMode';
import integrationsBgUrl from '@/assets/Images/integrations-hero-bg.png';
import integrationsHeroBgUrl from '@/assets/Images/integrations-hero-bg.png';
import awsDarkUrl from '@/assets/Logos/aws-dark.svg';
import AccountActions from './components/AccountActions';
@@ -15,7 +15,7 @@ function HeroSection(): JSX.Element {
style={
isDarkMode
? {
backgroundImage: `url(${integrationsBgUrl})`,
backgroundImage: `url('${integrationsHeroBgUrl}')`,
}
: {}
}

View File

@@ -7,6 +7,7 @@ import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { isModifierKeyPressed } from 'utils/app';
import { openExternalLink } from 'utils/navigation';
import { getOptionList } from './config';
import { AlertTypeCard, SelectTypeContainer } from './styles';
@@ -55,7 +56,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
page: 'New alert data source selection page',
});
window.open(url, '_blank');
openExternalLink(url);
}
const renderOptions = useMemo(
() => (

View File

@@ -14,6 +14,7 @@ import { IUser } from 'providers/App/types';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { USER_ROLES } from 'types/roles';
import { openInNewTab } from 'utils/navigation';
import { ROUTING_POLICIES_ROUTE } from './constants';
import { RoutingPolicyBannerProps } from './types';
@@ -387,7 +388,7 @@ export function NotificationChannelsNotFoundContent({
style={{ padding: '0 4px' }}
type="link"
onClick={(): void => {
window.open(ROUTES.CHANNELS_NEW, '_blank');
openInNewTab(ROUTES.CHANNELS_NEW);
}}
>
here.

View File

@@ -21,6 +21,7 @@ import { useGetHosts, usePutHost } from 'api/generated/services/zeus';
import { AxiosError } from 'axios';
import { useAppContext } from 'providers/App/App';
import { useTimezone } from 'providers/Timezone';
import { openExternalLink } from 'utils/navigation';
import CustomDomainEditModal from './CustomDomainEditModal';
@@ -48,7 +49,7 @@ function DomainUpdateToast({
className="custom-domain-toast-visit-btn"
suffixIcon={<ExternalLink size={12} />}
onClick={(): void => {
window.open(url, '_blank', 'noopener,noreferrer');
openExternalLink(url);
}}
>
Visit new URL

View File

@@ -16,6 +16,7 @@ import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
import APIError from 'types/api/error';
import { USER_ROLES } from 'types/roles';
import { openInNewTab } from 'utils/navigation';
import './PublicDashboard.styles.scss';
@@ -294,7 +295,7 @@ function PublicDashboardSetting(): JSX.Element {
icon={<ExternalLink size={12} />}
onClick={(): void => {
if (publicDashboardURL) {
window.open(publicDashboardURL, '_blank');
openInNewTab(publicDashboardURL);
}
}}
/>

View File

@@ -55,7 +55,12 @@ function PanelTypeSelectionModal(): JSX.Element {
>
<div className="panel-selection">
{PanelTypesWithData.map(({ name, icon, display }) => (
<Card onClick={(): void => handleCardClick(name)} id={name} key={name}>
<Card
onClick={(): void => handleCardClick(name)}
id={name}
key={name}
data-testid={`panel-type-${name}`}
>
{icon}
<Typography className="panel-type-text">{display}</Typography>
</Card>

View File

@@ -15,6 +15,7 @@ import { AlertDef, Labels } from 'types/api/alerts/def';
import { Channels } from 'types/api/channels/getAll';
import APIError from 'types/api/error';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
import { openInNewTab } from 'utils/navigation';
import { popupContainer } from 'utils/selectPopupContainer';
import ChannelSelect from './ChannelSelect';
@@ -87,7 +88,7 @@ function BasicInfo({
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
ruleId: isNewRule ? 0 : alertDef?.id,
});
window.open(ROUTES.CHANNELS_NEW, '_blank');
openInNewTab(ROUTES.CHANNELS_NEW);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const hasLoggedEvent = useRef(false);

View File

@@ -1,4 +1,4 @@
import ClickHouseQueryBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query';
import ClickHouseQueryBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/ClickHouse/query';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
function ChQuerySection(): JSX.Element {

View File

@@ -46,6 +46,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed } from 'utils/app';
import { compositeQueryToQueryEnvelope } from 'utils/compositeQueryToQueryEnvelope';
import { openExternalLink } from 'utils/navigation';
import BasicInfo from './BasicInfo';
import ChartPreview from './ChartPreview';
@@ -771,7 +772,7 @@ function FormAlertRules({
queryType: currentQuery.queryType,
link: url,
});
window.open(url, '_blank');
openExternalLink(url);
}
}

View File

@@ -13,6 +13,7 @@ import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { GettableAlert } from 'types/api/alerts/get';
import { USER_ROLES } from 'types/roles';
import { openExternalLink } from 'utils/navigation';
import beaconUrl from '@/assets/Icons/beacon.svg';
import circusTentUrl from '@/assets/Icons/circus-tent.svg';
@@ -103,11 +104,7 @@ export default function AlertRules({
source: 'Alert Rules',
});
window.open(
'https://signoz.io/docs/alerts/',
'_blank',
'noreferrer noopener',
);
openExternalLink('https://signoz.io/docs/alerts/');
}}
>
Learn more <ArrowUpRight size={12} />

View File

@@ -10,6 +10,7 @@ import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { Dashboard } from 'types/api/dashboard/getAll';
import { USER_ROLES } from 'types/roles';
import { openExternalLink, openInNewTab } from 'utils/navigation';
import circusTentUrl from '@/assets/Icons/circus-tent.svg';
import dialsUrl from '@/assets/Icons/dials.svg';
@@ -87,10 +88,7 @@ export default function Dashboards({
logEvent('Homepage: Learn more clicked', {
source: 'Dashboards',
});
window.open(
'https://signoz.io/docs/userguide/manage-dashboards/',
'_blank',
);
openExternalLink('https://signoz.io/docs/userguide/manage-dashboards/');
}}
>
Learn more <ArrowUpRight size={12} />
@@ -114,7 +112,7 @@ export default function Dashboards({
dashboardName: dashboard.data.title,
});
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
openInNewTab(getLink());
} else {
safeNavigate(getLink());
}

View File

@@ -9,6 +9,7 @@ import { Link2 } from 'lucide-react';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { openExternalLink } from 'utils/navigation';
import containerPlusUrl from '@/assets/Icons/container-plus.svg';
import helloWaveUrl from '@/assets/Icons/hello-wave.svg';
@@ -51,7 +52,7 @@ function DataSourceInfo({
if (activeLicense && activeLicense.platform === LicensePlatform.CLOUD) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(DOCS_LINKS.ADD_DATA_SOURCE, '_blank', 'noopener noreferrer');
openExternalLink(DOCS_LINKS.ADD_DATA_SOURCE);
}
};

View File

@@ -8,6 +8,7 @@ import { ArrowRight, ArrowRightToLine, BookOpenText } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { USER_ROLES } from 'types/roles';
import { openExternalLink } from 'utils/navigation';
import './HomeChecklist.styles.scss';
@@ -99,11 +100,7 @@ function HomeChecklist({
) {
history.push(item.toRoute || '');
} else {
window?.open(
item.docsLink || '',
'_blank',
'noopener noreferrer',
);
openExternalLink(item.docsLink || '');
}
}}
>
@@ -119,7 +116,7 @@ function HomeChecklist({
step: item.id,
});
window?.open(item.docsLink, '_blank', 'noopener noreferrer');
openExternalLink(item.docsLink || '');
}}
>
<BookOpenText size={16} />

View File

@@ -19,6 +19,7 @@ import { useAppContext } from 'providers/App/App';
import { ViewProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
import { openExternalLink } from 'utils/navigation';
import circusTentUrl from '@/assets/Icons/circus-tent.svg';
import eightBallUrl from '@/assets/Icons/eight-ball.svg';
@@ -196,11 +197,7 @@ export default function SavedViews({
entity: selectedEntity,
});
window.open(
'https://signoz.io/docs/product-features/saved-view/',
'_blank',
'noopener noreferrer',
);
openExternalLink('https://signoz.io/docs/product-features/saved-view/');
}}
>
Learn more <ArrowUpRight size={12} />

View File

@@ -31,6 +31,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed } from 'utils/app';
import { openExternalLink } from 'utils/navigation';
import triangleRulerUrl from '@/assets/Icons/triangle-ruler.svg';
@@ -79,11 +80,7 @@ const EmptyState = memo(
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',
);
openExternalLink(DOCS_LINKS.ADD_DATA_SOURCE);
}
}}
>
@@ -97,10 +94,7 @@ const EmptyState = memo(
logEvent('Homepage: Learn more clicked', {
source: 'Service Metrics',
});
window.open(
'https://signoz.io/docs/instrumentation/overview/',
'_blank',
);
openExternalLink('https://signoz.io/docs/instrumentation/overview/');
}}
>
Learn more <ArrowUpRight size={12} />

View File

@@ -17,6 +17,7 @@ import { ServicesList } from 'types/api/metrics/getService';
import { GlobalReducer } from 'types/reducer/globalTime';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed } from 'utils/app';
import { openExternalLink } from 'utils/navigation';
import triangleRulerUrl from '@/assets/Icons/triangle-ruler.svg';
@@ -133,11 +134,7 @@ export default function ServiceTraces({
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',
);
openExternalLink(DOCS_LINKS.ADD_DATA_SOURCE);
}
}}
>
@@ -151,10 +148,7 @@ export default function ServiceTraces({
logEvent('Homepage: Learn more clicked', {
source: 'Service Traces',
});
window.open(
'https://signoz.io/docs/instrumentation/overview/',
'_blank',
);
openExternalLink('https://signoz.io/docs/instrumentation/overview/');
}}
>
Learn more <ArrowUpRight size={12} />

View File

@@ -49,6 +49,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import ClusterEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -414,10 +415,7 @@ function ClusterDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -436,10 +434,7 @@ function ClusterDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -48,6 +48,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import DaemonSetEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -429,10 +430,7 @@ function DaemonSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -451,10 +449,7 @@ function DaemonSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import DeploymentEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -433,10 +434,7 @@ function DeploymentDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -455,10 +453,7 @@ function DeploymentDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -48,6 +48,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import JobEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -427,10 +428,7 @@ function JobDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -449,10 +447,7 @@ function JobDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import NamespaceEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -419,10 +420,7 @@ function NamespaceDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -441,10 +439,7 @@ function NamespaceDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import NodeLogs from '../../EntityDetailsUtils/EntityLogs';
@@ -416,10 +417,7 @@ function NodeDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -438,10 +436,7 @@ function NodeDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import PodEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -435,10 +436,7 @@ function PodDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -457,10 +455,7 @@ function PodDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -53,6 +53,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import {
@@ -431,10 +432,7 @@ function StatefulSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -453,10 +451,7 @@ function StatefulSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -1,5 +1,6 @@
import { ArrowRightOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import { openExternalLink } from 'utils/navigation';
interface AlertInfoCardProps {
header: string;
@@ -19,7 +20,7 @@ function AlertInfoCard({
className="alert-info-card"
onClick={(): void => {
onClick();
window.open(link, '_blank');
openExternalLink(link);
}}
>
<div className="alert-card-text">

View File

@@ -1,5 +1,6 @@
import { ArrowRightOutlined, PlayCircleFilled } from '@ant-design/icons';
import { Flex, Typography } from 'antd';
import { openExternalLink } from 'utils/navigation';
interface InfoLinkTextProps {
infoText: string;
@@ -20,7 +21,7 @@ function InfoLinkText({
<Flex
onClick={(): void => {
onClick();
window.open(link, '_blank');
openExternalLink(link);
}}
className="info-link-container"
>

View File

@@ -83,6 +83,7 @@ import {
} from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { isModifierKeyPressed } from 'utils/app';
import { openExternalLink, openInNewTab } from 'utils/navigation';
import awwSnapUrl from '@/assets/Icons/awwSnap.svg';
import dashboardsUrl from '@/assets/Icons/dashboards.svg';
@@ -457,7 +458,7 @@ function DashboardsList(): JSX.Element {
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
window.open(getLink(), '_blank');
openInNewTab(getLink());
}}
>
Open in New Tab
@@ -564,6 +565,7 @@ function DashboardsList(): JSX.Element {
label: (
<div
className="create-dashboard-menu-item"
data-testid="import-json-menu-cta"
onClick={(): void => onModalHandler(false)}
>
<Radius size={14} /> Import JSON
@@ -577,6 +579,7 @@ function DashboardsList(): JSX.Element {
href="https://signoz.io/docs/dashboards/dashboard-templates/overview/"
target="_blank"
rel="noopener noreferrer"
data-testid="view-templates-menu-cta"
>
<Flex
justify="space-between"
@@ -600,6 +603,7 @@ function DashboardsList(): JSX.Element {
label: (
<div
className="create-dashboard-menu-item"
data-testid="create-dashboard-menu-cta"
onClick={(): void => {
onNewDashboardHandler();
}}
@@ -736,9 +740,8 @@ function DashboardsList(): JSX.Element {
className="learn-more"
data-testid="learn-more"
onClick={(): void => {
window.open(
openExternalLink(
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
'_blank',
);
}}
>
@@ -755,6 +758,7 @@ function DashboardsList(): JSX.Element {
placeholder="Search by name, description, or tags..."
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
value={searchString}
data-testid="dashboards-list-search"
onChange={handleSearch}
/>
{createNewDashboard && (
@@ -768,6 +772,7 @@ function DashboardsList(): JSX.Element {
type="primary"
className="periscope-btn primary btn"
icon={<Plus size={14} />}
data-testid="new-dashboard-cta"
onClick={(): void => {
logEvent('Dashboard List: New dashboard clicked', {});
}}

View File

@@ -1,6 +1,7 @@
import { LockFilled } from '@ant-design/icons';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { openInNewTab } from 'utils/navigation';
import { Data } from '../DashboardsList';
import { TableLinkText } from './styles';
@@ -12,7 +13,7 @@ function Name(name: Data['name'], data: Data): JSX.Element {
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
openInNewTab(getLink());
} else {
history.push(getLink());
}

View File

@@ -17,6 +17,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { openInNewTab } from 'utils/navigation';
import { useContextLogData } from './useContextLogData';
@@ -116,7 +117,7 @@ function ContextLogRenderer({
);
const link = `${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`;
window.open(link, '_blank', 'noopener,noreferrer');
openInNewTab(link);
},
[query, urlQuery],
);

View File

@@ -34,6 +34,7 @@ import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { openInNewTab } from 'utils/navigation';
import { ActionItemProps } from './ActionItem';
import FieldRenderer from './FieldRenderer';
@@ -191,7 +192,7 @@ function TableView({
if (event.ctrlKey || event.metaKey) {
// open the trace in new tab
window.open(route, '_blank');
openInNewTab(route);
} else {
history.push(route);
}

View File

@@ -2,6 +2,7 @@ import { Typography } from 'antd';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
import { ArrowRight } from 'lucide-react';
import { openExternalLink } from 'utils/navigation';
import awwSnapUrl from '@/assets/Icons/awwSnap.svg';
@@ -14,7 +15,7 @@ export default function LogsError(): JSX.Element {
if (isCloudUserVal) {
history.push('/support');
} else {
window.open('https://signoz.io/slack', '_blank');
openExternalLink('https://signoz.io/slack');
}
};

View File

@@ -1,5 +1,6 @@
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { openInNewTab as openInNewTabFn } from 'utils/navigation';
import { TopOperationList } from './TopOperationsTable';
import { NavigateToTraceProps } from './types';
@@ -37,7 +38,7 @@ export const navigateToTrace = ({
}=${JSONCompositeQuery}`;
if (openInNewTab) {
window.open(newTraceExplorerPath, '_blank');
openInNewTabFn(newTraceExplorerPath);
} else {
safeNavigate(newTraceExplorerPath);
}

View File

@@ -9,6 +9,7 @@ import {
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { Bell, Grid } from 'lucide-react';
import { openInNewTab } from 'utils/navigation';
import { pluralize } from 'utils/pluralize';
import { DashboardsAndAlertsPopoverProps } from './types';
@@ -67,9 +68,8 @@ function DashboardsAndAlertsPopover({
<Typography.Link
key={alert.alertId}
onClick={(): void => {
window.open(
openInNewTab(
`${ROUTES.ALERT_OVERVIEW}?${QueryParams.ruleId}=${alert.alertId}`,
'_blank',
);
}}
className="dashboards-popover-content-item"
@@ -90,11 +90,8 @@ function DashboardsAndAlertsPopover({
<Typography.Link
key={dashboard.dashboardId}
onClick={(): void => {
window.open(
generatePath(ROUTES.DASHBOARD, {
dashboardId: dashboard.dashboardId,
}),
'_blank',
openInNewTab(
generatePath(ROUTES.DASHBOARD, { dashboardId: dashboard.dashboardId }),
);
}}
className="dashboards-popover-content-item"

View File

@@ -0,0 +1,7 @@
.info-banner-wrapper {
margin: 8px 8px 16px 16px;
a {
text-decoration: underline;
}
}

View File

@@ -1,10 +1,14 @@
import { PlusOutlined } from '@ant-design/icons';
import { Callout } from '@signozhq/ui';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { EQueryType } from 'types/common/dashboard';
import DOCLINKS from 'utils/docLinks';
import { QueryButton } from '../../styles';
import ClickHouseQueryBuilder from './query';
import './ClickHouse.styles.scss';
function ClickHouseQueryContainer(): JSX.Element | null {
const { currentQuery, addNewQueryItem } = useQueryBuilder();
const addQueryHandler = (): void => {
@@ -13,6 +17,28 @@ function ClickHouseQueryContainer(): JSX.Element | null {
return (
<>
<div className="info-banner-wrapper">
<Callout
type="info"
showIcon
title={
<span>
<a
href={DOCLINKS.QUERY_CLICKHOUSE_TRACES}
target="_blank"
rel="noreferrer"
>
Learn to write faster, optimized queries
</a>
{' · Using AI? '}
<a href={DOCLINKS.AGENT_SKILL_INSTALL} target="_blank" rel="noreferrer">
Install the SigNoz ClickHouse query agent skill
</a>
</span>
}
/>
</div>
{currentQuery.clickhouse_sql.map((q, idx) => (
<ClickHouseQueryBuilder
key={q.name}

View File

@@ -19,7 +19,7 @@ import { Atom, Terminal } from 'lucide-react';
import { Widgets } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
import ClickHouseQueryContainer from './QueryBuilder/ClickHouse';
import PromQLQueryContainer from './QueryBuilder/promQL';
import './QuerySection.styles.scss';

View File

@@ -13,6 +13,7 @@ interface ColumnUnitSelectorProps {
columnUnits: ColumnUnit;
setColumnUnits: Dispatch<SetStateAction<ColumnUnit>>;
isNewDashboard: boolean;
'data-testid'?: string;
}
export function ColumnUnitSelector(
@@ -83,6 +84,7 @@ export function ColumnUnitSelector(
}
fieldLabel={label}
key={value}
data-testid={props['data-testid']}
selectedQueryName={baseQueryName}
// Update the column unit value automatically only in create mode
shouldUpdateYAxisUnit={isNewDashboard}

View File

@@ -138,6 +138,7 @@ function ContextLinks({
<Button
type="default"
className="add-context-link-button"
data-testid="add-context-link-cta"
icon={<Plus size={12} />}
style={{ width: '100%' }}
onClick={handleAddContextLink}

View File

@@ -15,12 +15,14 @@ function DashboardYAxisUnitSelectorWrapper({
fieldLabel,
shouldUpdateYAxisUnit,
selectedQueryName,
'data-testid': dataTestId,
}: {
value: string;
onSelect: OnSelectType;
fieldLabel: string;
shouldUpdateYAxisUnit: boolean;
selectedQueryName?: string;
'data-testid'?: string;
}): JSX.Element {
const { yAxisUnit: initialYAxisUnit, isLoading } = useGetYAxisUnit(
selectedQueryName,
@@ -42,6 +44,7 @@ function DashboardYAxisUnitSelectorWrapper({
initialValue={initialYAxisUnit}
source={YAxisSource.DASHBOARDS}
loading={isLoading}
data-testid={dataTestId}
/>
</div>
);

View File

@@ -66,6 +66,7 @@ export default function FormattingUnitsSection({
options={decimapPrecisionOptions}
value={decimalPrecision}
className="panel-type-select"
data-testid="decimal-precision-selector"
defaultValue={decimapPrecisionOptions[0]?.value}
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
/>
@@ -77,6 +78,7 @@ export default function FormattingUnitsSection({
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
isNewDashboard={isNewDashboard}
data-testid="column-unit-selector"
/>
)}
</SettingsSection>

View File

@@ -135,6 +135,7 @@ export default function GeneralSettingsSection({
rootClassName="general-settings__name-input"
ref={inputRef}
onSelect={handleInputCursor}
data-testid="panel-name-input"
onClick={handleInputCursor}
onBlur={(): void => setAutoCompleteOpen(false)}
/>
@@ -145,6 +146,7 @@ export default function GeneralSettingsSection({
bordered
allowClear
value={description}
data-testid="panel-description-input"
onChange={(event): void =>
onChangeHandler(setDescription, event.target.value)
}

View File

@@ -249,6 +249,7 @@ function Threshold({
<Input
defaultValue={label}
onChange={handleLabelChange}
data-testid="threshold-label-input"
bordered={!isDarkMode}
className="label-input"
/>
@@ -275,6 +276,7 @@ function Threshold({
onChange={handleTableOptionsChange}
rootClassName="operator-input-root"
className="operator-input"
data-testid="table-operator-input-selector"
/>
<Typography.Text className="typography">is</Typography.Text>
</Space>
@@ -287,6 +289,7 @@ function Threshold({
style={{ marginLeft: '10px' }}
rootClassName="operator-input-root"
className="operator-input"
data-testid="operator-input-selector"
/>
</div>
) : (
@@ -321,6 +324,7 @@ function Threshold({
defaultValue={value}
onChange={handleValueChange}
className="unit-input"
data-testid="threshold-value-input"
/>
) : (
<ShowCaseValue value={value} className="unit-input" />
@@ -332,6 +336,7 @@ function Threshold({
placeholder="Select unit"
source={YAxisSource.DASHBOARDS}
initialValue={unit}
data-testid="threshold-unit-input"
categoriesOverride={unitSelectCategories}
containerClassName="unit-selection"
/>
@@ -349,6 +354,7 @@ function Threshold({
defaultValue={format}
options={showAsOptions}
onChange={handlerFormatChange}
data-testid="threshold-color-selector"
rootClassName="color-format"
/>
</>

View File

@@ -72,6 +72,7 @@ function ThresholdSelector({
type="default"
icon={<Plus size={14} />}
style={{ width: '100%' }}
data-testid="add-threshold-cta"
onClick={addThresholdHandler}
>
Add Threshold

View File

@@ -677,6 +677,18 @@ function NewWidget({
queryType: currentQuery.queryType,
isNewPanel,
dataSource: currentQuery?.builder?.queryData?.[0]?.dataSource,
...(currentQuery.queryType === EQueryType.CLICKHOUSE && {
clickhouseQueryCount: currentQuery.clickhouse_sql.length,
clickhouseQueries: currentQuery.clickhouse_sql.map((q) => ({
name: q.name,
query: (q.query ?? '')
.replace(/--[^\n]*/g, '') // strip line comments
.replace(/\/\*[\s\S]*?\*\//g, '') // strip block comments
.replace(/'(?:[^'\\]|\\.|'')*'/g, "'?'") // replace single-quoted strings (handles \' and '' escapes)
.replace(/\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/g, '?'), // replace numeric literals (int, float, scientific)
disabled: q.disabled,
})),
}),
});
setSaveModal(true);
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -6,6 +6,7 @@ import history from 'lib/history';
import { ArrowUpRight } from 'lucide-react';
import { DataSource } from 'types/common/queryBuilder';
import DOCLINKS from 'utils/docLinks';
import { openExternalLink } from 'utils/navigation';
import eyesEmojiUrl from '@/assets/Images/eyesEmoji.svg';
@@ -42,11 +43,11 @@ export default function NoLogs({
}
history.push(link);
} else if (dataSource === 'traces') {
window.open(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE, '_blank');
openExternalLink(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE);
} else if (dataSource === DataSource.METRICS) {
window.open(DOCLINKS.METRICS_EXPLORER_EMPTY_STATE, '_blank');
openExternalLink(DOCLINKS.METRICS_EXPLORER_EMPTY_STATE);
} else {
window.open(`${DOCLINKS.USER_GUIDE}${dataSource}/`, '_blank');
openExternalLink(`${DOCLINKS.USER_GUIDE}${dataSource}/`);
}
};
return (

View File

@@ -23,14 +23,14 @@ import { PayloadProps as QueryServicePayloadProps } from 'types/api/metrics/getS
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import elixirUrl from '@/assets/Logos/elixir.png';
import goUrl from '@/assets/Logos/go.png';
import javaUrl from '@/assets/Logos/java.png';
import javascriptUrl from '@/assets/Logos/javascript.png';
import pythonUrl from '@/assets/Logos/python.png';
import railsUrl from '@/assets/Logos/rails.png';
import rustUrl from '@/assets/Logos/rust.png';
import swiftUrl from '@/assets/Logos/swift.png';
import elixirPngUrl from '@/assets/Logos/elixir.png';
import goPngUrl from '@/assets/Logos/go.png';
import javaPngUrl from '@/assets/Logos/java.png';
import javascriptPngUrl from '@/assets/Logos/javascript.png';
import pythonPngUrl from '@/assets/Logos/python.png';
import railsPngUrl from '@/assets/Logos/rails.png';
import rustPngUrl from '@/assets/Logos/rust.png';
import swiftPngUrl from '@/assets/Logos/swift.png';
import './ConnectionStatus.styles.scss';
@@ -148,7 +148,7 @@ export default function ConnectionStatus(): JSX.Element {
<Header
entity="java"
heading="Java OpenTelemetry Instrumentation"
imgURL={javaUrl}
imgURL={javaPngUrl}
docsURL="https://signoz.io/docs/instrumentation/java/"
imgClassName="supported-language-img"
/>
@@ -159,7 +159,7 @@ export default function ConnectionStatus(): JSX.Element {
<Header
entity="python"
heading="Python OpenTelemetry Instrumentation"
imgURL={pythonUrl}
imgURL={pythonPngUrl}
docsURL="https://signoz.io/docs/instrumentation/python/"
imgClassName="supported-language-img"
/>
@@ -170,7 +170,7 @@ export default function ConnectionStatus(): JSX.Element {
<Header
entity="javascript"
heading="Javascript OpenTelemetry Instrumentation"
imgURL={javascriptUrl}
imgURL={javascriptPngUrl}
docsURL="https://signoz.io/docs/instrumentation/javascript/"
imgClassName="supported-language-img"
/>
@@ -180,7 +180,7 @@ export default function ConnectionStatus(): JSX.Element {
<Header
entity="go"
heading="Go OpenTelemetry Instrumentation"
imgURL={goUrl}
imgURL={goPngUrl}
docsURL="https://signoz.io/docs/instrumentation/golang/"
imgClassName="supported-language-img"
/>
@@ -190,7 +190,7 @@ export default function ConnectionStatus(): JSX.Element {
<Header
entity="rails"
heading="Ruby on Rails OpenTelemetry Instrumentation"
imgURL={railsUrl}
imgURL={railsPngUrl}
docsURL="https://signoz.io/docs/instrumentation/ruby-on-rails/"
imgClassName="supported-language-img"
/>
@@ -200,7 +200,7 @@ export default function ConnectionStatus(): JSX.Element {
<Header
entity="rust"
heading="Rust OpenTelemetry Instrumentation"
imgURL={rustUrl}
imgURL={rustPngUrl}
docsURL="https://signoz.io/docs/instrumentation/rust/"
imgClassName="supported-language-img"
/>
@@ -210,7 +210,7 @@ export default function ConnectionStatus(): JSX.Element {
<Header
entity="rust"
heading="Elixir OpenTelemetry Instrumentation"
imgURL={elixirUrl}
imgURL={elixirPngUrl}
docsURL="https://signoz.io/docs/instrumentation/elixir/"
imgClassName="supported-language-img"
/>
@@ -220,7 +220,7 @@ export default function ConnectionStatus(): JSX.Element {
<Header
entity="swift"
heading="Swift OpenTelemetry Instrumentation"
imgURL={swiftUrl}
imgURL={swiftPngUrl}
docsURL="https://signoz.io/docs/instrumentation/swift/"
imgClassName="supported-language-img"
/>

View File

@@ -18,12 +18,12 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import cmdTerminalUrl from '@/assets/Logos/cmd-terminal.svg';
import dockerUrl from '@/assets/Logos/docker.svg';
import kubernetesUrl from '@/assets/Logos/kubernetes.svg';
import nodeJsUrl from '@/assets/Logos/node-js.svg';
import softwareWindowUrl from '@/assets/Logos/software-window.svg';
import syslogsUrl from '@/assets/Logos/syslogs.svg';
import cmdTerminalSvgUrl from '@/assets/Logos/cmd-terminal.svg';
import dockerSvgUrl from '@/assets/Logos/docker.svg';
import kubernetesSvgUrl from '@/assets/Logos/kubernetes.svg';
import nodeJsSvgUrl from '@/assets/Logos/node-js.svg';
import softwareWindowSvgUrl from '@/assets/Logos/software-window.svg';
import syslogsSvgUrl from '@/assets/Logos/syslogs.svg';
import './LogsConnectionStatus.styles.scss';
@@ -172,7 +172,7 @@ export default function LogsConnectionStatus(): JSX.Element {
<Header
entity="kubernetes"
heading="Collecting Kubernetes Pod logs"
imgURL={kubernetesUrl}
imgURL={kubernetesSvgUrl}
docsURL="https://signoz.io/docs/userguide/collect_kubernetes_pod_logs/#collect-kubernetes-pod-logs-in-signoz-cloud"
imgClassName="supported-logs-type-img"
/>
@@ -183,7 +183,7 @@ export default function LogsConnectionStatus(): JSX.Element {
<Header
entity="docker"
heading="Collecting Docker container logs"
imgURL={dockerUrl}
imgURL={dockerSvgUrl}
docsURL="https://signoz.io/docs/userguide/collect_docker_logs/"
imgClassName="supported-logs-type-img"
/>
@@ -194,7 +194,7 @@ export default function LogsConnectionStatus(): JSX.Element {
<Header
entity="syslog"
heading="Collecting Syslogs"
imgURL={syslogsUrl}
imgURL={syslogsSvgUrl}
docsURL="https://signoz.io/docs/userguide/collecting_syslogs/"
imgClassName="supported-logs-type-img"
/>
@@ -204,7 +204,7 @@ export default function LogsConnectionStatus(): JSX.Element {
<Header
entity="nodejs"
heading="Collecting NodeJS winston logs"
imgURL={nodeJsUrl}
imgURL={nodeJsSvgUrl}
docsURL="https://signoz.io/docs/userguide/collecting_nodejs_winston_logs/"
imgClassName="supported-logs-type-img"
/>
@@ -221,8 +221,8 @@ export default function LogsConnectionStatus(): JSX.Element {
}
imgURL={
logType === ApplicationLogsType.FROM_LOG_FILE
? softwareWindowUrl
: cmdTerminalUrl
? softwareWindowSvgUrl
: cmdTerminalSvgUrl
}
docsURL={
logType === ApplicationLogsType.FROM_LOG_FILE

View File

@@ -1,37 +1,37 @@
import ROUTES from 'constants/routes';
import azureAksUrl from '@/assets/Logos/azure-aks.svg';
import azureAppServiceUrl from '@/assets/Logos/azure-app-service.svg';
import azureBlobStorageUrl from '@/assets/Logos/azure-blob-storage.svg';
import azureContainerAppsUrl from '@/assets/Logos/azure-container-apps.svg';
import azureFunctionsUrl from '@/assets/Logos/azure-functions.svg';
import azureSqlDatabaseMetricsUrl from '@/assets/Logos/azure-sql-database-metrics.svg';
import azureVmUrl from '@/assets/Logos/azure-vm.svg';
import cloudwatchUrl from '@/assets/Logos/cloudwatch.png';
import cmdTerminalUrl from '@/assets/Logos/cmd-terminal.svg';
import dockerUrl from '@/assets/Logos/docker.svg';
import dotnetUrl from '@/assets/Logos/dotnet.png';
import ec2Url from '@/assets/Logos/ec2.svg';
import ecsUrl from '@/assets/Logos/ecs.svg';
import eksUrl from '@/assets/Logos/eks.svg';
import elixirUrl from '@/assets/Logos/elixir.png';
import fluentBitUrl from '@/assets/Logos/fluent-bit.png';
import fluentdUrl from '@/assets/Logos/fluentd.png';
import goUrl from '@/assets/Logos/go.png';
import herokuUrl from '@/assets/Logos/heroku.png';
import httpUrl from '@/assets/Logos/http.png';
import javaUrl from '@/assets/Logos/java.png';
import javascriptUrl from '@/assets/Logos/javascript.png';
import kubernetesUrl from '@/assets/Logos/kubernetes.svg';
import logstashUrl from '@/assets/Logos/logstash.svg';
import phpUrl from '@/assets/Logos/php.png';
import pythonUrl from '@/assets/Logos/python.png';
import railsUrl from '@/assets/Logos/rails.png';
import rustUrl from '@/assets/Logos/rust.png';
import softwareWindowUrl from '@/assets/Logos/software-window.svg';
import swiftUrl from '@/assets/Logos/swift.png';
import syslogsUrl from '@/assets/Logos/syslogs.svg';
import vercelUrl from '@/assets/Logos/vercel.png';
import azureAksSvgUrl from '@/assets/Logos/azure-aks.svg';
import azureAppServiceSvgUrl from '@/assets/Logos/azure-app-service.svg';
import azureBlobStorageSvgUrl from '@/assets/Logos/azure-blob-storage.svg';
import azureContainerAppsSvgUrl from '@/assets/Logos/azure-container-apps.svg';
import azureFunctionsSvgUrl from '@/assets/Logos/azure-functions.svg';
import azureSqlDatabaseMetricsSvgUrl from '@/assets/Logos/azure-sql-database-metrics.svg';
import azureVmSvgUrl from '@/assets/Logos/azure-vm.svg';
import cloudwatchPngUrl from '@/assets/Logos/cloudwatch.png';
import cmdTerminalSvgUrl from '@/assets/Logos/cmd-terminal.svg';
import dockerSvgUrl from '@/assets/Logos/docker.svg';
import dotnetPngUrl from '@/assets/Logos/dotnet.png';
import ec2SvgUrl from '@/assets/Logos/ec2.svg';
import ecsSvgUrl from '@/assets/Logos/ecs.svg';
import eksSvgUrl from '@/assets/Logos/eks.svg';
import elixirPngUrl from '@/assets/Logos/elixir.png';
import fluentBitPngUrl from '@/assets/Logos/fluent-bit.png';
import fluentdPngUrl from '@/assets/Logos/fluentd.png';
import goPngUrl from '@/assets/Logos/go.png';
import herokuPngUrl from '@/assets/Logos/heroku.png';
import httpPngUrl from '@/assets/Logos/http.png';
import javaPngUrl from '@/assets/Logos/java.png';
import javascriptPngUrl from '@/assets/Logos/javascript.png';
import kubernetesSvgUrl from '@/assets/Logos/kubernetes.svg';
import logstashSvgUrl from '@/assets/Logos/logstash.svg';
import phpPngUrl from '@/assets/Logos/php.png';
import pythonPngUrl from '@/assets/Logos/python.png';
import railsPngUrl from '@/assets/Logos/rails.png';
import rustPngUrl from '@/assets/Logos/rust.png';
import softwareWindowSvgUrl from '@/assets/Logos/software-window.svg';
import swiftPngUrl from '@/assets/Logos/swift.png';
import syslogsSvgUrl from '@/assets/Logos/syslogs.svg';
import vercelPngUrl from '@/assets/Logos/vercel.png';
import { ModuleProps } from '../OnboardingContainer';
import { DataSourceType } from '../Steps/DataSource/DataSource';
@@ -122,230 +122,230 @@ export const frameworksMap = {
export const defaultApplicationDataSource = {
name: 'java',
id: 'java',
imgURL: javaUrl,
imgURL: javaPngUrl,
};
const supportedLanguages = [
{
name: 'java',
id: 'java',
imgURL: javaUrl,
imgURL: javaPngUrl,
},
{
name: 'python',
id: 'python',
imgURL: pythonUrl,
imgURL: pythonPngUrl,
},
{
name: 'go',
id: 'go',
imgURL: goUrl,
imgURL: goPngUrl,
},
{
name: 'javascript',
id: 'javascript',
imgURL: javascriptUrl,
imgURL: javascriptPngUrl,
},
{
name: 'rails',
id: 'rails',
imgURL: railsUrl,
imgURL: railsPngUrl,
},
{
name: '.NET',
id: 'dotnet',
imgURL: dotnetUrl,
imgURL: dotnetPngUrl,
},
{
name: 'rust',
id: 'rust',
imgURL: rustUrl,
imgURL: rustPngUrl,
},
{
name: 'elixir',
id: 'elixir',
imgURL: elixirUrl,
imgURL: elixirPngUrl,
},
{
name: 'swift',
id: 'swift',
imgURL: swiftUrl,
imgURL: swiftPngUrl,
},
{
name: 'php',
id: 'php',
imgURL: phpUrl,
imgURL: phpPngUrl,
},
];
export const defaultLogsType = {
name: 'Kubernetes Pod Logs',
id: 'kubernetes',
imgURL: kubernetesUrl,
imgURL: kubernetesSvgUrl,
};
const supportedLogsTypes = [
{
name: 'Kubernetes Pod Logs',
id: 'kubernetes',
imgURL: kubernetesUrl,
imgURL: kubernetesSvgUrl,
},
{
name: 'Docker Container Logs',
id: 'docker',
imgURL: dockerUrl,
imgURL: dockerSvgUrl,
},
{
name: 'SysLogs',
id: 'syslogs',
imgURL: syslogsUrl,
imgURL: syslogsSvgUrl,
},
{
name: 'Application Logs',
id: 'application_logs',
imgURL: softwareWindowUrl,
imgURL: softwareWindowSvgUrl,
},
{
name: 'FluentBit',
id: 'fluentBit',
imgURL: fluentBitUrl,
imgURL: fluentBitPngUrl,
},
{
name: 'FluentD',
id: 'fluentD',
imgURL: fluentdUrl,
imgURL: fluentdPngUrl,
},
{
name: 'LogStash',
id: 'logStash',
imgURL: logstashUrl,
imgURL: logstashSvgUrl,
},
{
name: 'Heroku',
id: 'heroku',
imgURL: herokuUrl,
imgURL: herokuPngUrl,
},
{
name: 'Vercel',
id: 'vercel',
imgURL: vercelUrl,
imgURL: vercelPngUrl,
},
{
name: 'HTTP',
id: 'http',
imgURL: httpUrl,
imgURL: httpPngUrl,
},
{
name: 'Cloudwatch',
id: 'cloudwatch',
imgURL: cloudwatchUrl,
imgURL: cloudwatchPngUrl,
},
];
export const defaultInfraMetricsType = {
name: 'Kubernetes Infra Metrics',
id: 'kubernetesInfraMetrics',
imgURL: kubernetesUrl,
imgURL: kubernetesSvgUrl,
};
const supportedInfraMetrics = [
{
name: 'Kubernetes Infra Metrics',
id: 'kubernetesInfraMetrics',
imgURL: kubernetesUrl,
imgURL: kubernetesSvgUrl,
},
{
name: 'HostMetrics',
id: 'hostMetrics',
imgURL: softwareWindowUrl,
imgURL: softwareWindowSvgUrl,
},
{
name: 'Other Metrics',
id: 'otherMetrics',
imgURL: cmdTerminalUrl,
imgURL: cmdTerminalSvgUrl,
},
];
export const defaultAwsServices = {
name: 'EC2 - App/Server Logs',
id: 'awsEc2ApplicationLogs',
imgURL: ec2Url,
imgURL: ec2SvgUrl,
};
const supportedAwsServices = [
{
name: 'EC2 - App/Server Logs',
id: 'awsEc2ApplicationLogs',
imgURL: ec2Url,
imgURL: ec2SvgUrl,
},
{
name: 'EC2 - Infra Metrics',
id: 'awsEc2InfrastructureMetrics',
imgURL: ec2Url,
imgURL: ec2SvgUrl,
},
{
name: 'ECS - EC2',
id: 'awsEcsEc2',
imgURL: ecsUrl,
imgURL: ecsSvgUrl,
},
{
name: 'ECS - Fargate',
id: 'awsEcsFargate',
imgURL: ecsUrl,
imgURL: ecsSvgUrl,
},
{
name: 'ECS - External',
id: 'awsEcsExternal',
imgURL: ecsUrl,
imgURL: ecsSvgUrl,
},
{
name: 'EKS',
id: 'awsEks',
imgURL: eksUrl,
imgURL: eksSvgUrl,
},
];
export const defaultAzureServices = {
name: 'VM',
id: 'azureVm',
imgURL: azureVmUrl,
imgURL: azureVmSvgUrl,
};
const supportedAzureServices = [
{
name: 'VM',
id: 'azureVm',
imgURL: azureVmUrl,
imgURL: azureVmSvgUrl,
},
{
name: 'App Service',
id: 'azureAppService',
imgURL: azureAppServiceUrl,
imgURL: azureAppServiceSvgUrl,
},
{
name: 'AKS',
id: 'azureAks',
imgURL: azureAksUrl,
imgURL: azureAksSvgUrl,
},
{
name: 'Azure Functions',
id: 'azureFunctions',
imgURL: azureFunctionsUrl,
imgURL: azureFunctionsSvgUrl,
},
{
name: 'Azure Container Apps',
id: 'azureContainerApps',
imgURL: azureContainerAppsUrl,
imgURL: azureContainerAppsSvgUrl,
},
{
name: 'SQL Database Metrics',
id: 'azureSQLDatabaseMetrics',
imgURL: azureSqlDatabaseMetricsUrl,
imgURL: azureSqlDatabaseMetricsSvgUrl,
},
{
name: 'Azure Blob Storage',
id: 'azureBlobStorage',
imgURL: azureBlobStorageUrl,
imgURL: azureBlobStorageSvgUrl,
},
];

View File

@@ -1,3 +1,5 @@
/* eslint-disable rulesdir/no-unsupported-asset-pattern */
// hippa.svg and soc2.svg do not exist in src/assets — suppressed until assets are added
import { Dot } from 'lucide-react';
import './OnboardingFooter.styles.scss';

View File

@@ -4,4 +4,5 @@ import { DataSource } from 'types/common/queryBuilder';
export type QueryLabelProps = {
onChange: (value: DataSource) => void;
isListViewPanel?: boolean;
'data-testid'?: string;
} & Omit<SelectProps, 'onChange'>;

View File

@@ -32,6 +32,7 @@ export const DataSourceDropdown = memo(function DataSourceDropdown(
defaultValue={dataSourceOptions[0].value}
options={dataSourceOptions}
onChange={onChange}
data-testid={props['data-testid']}
value={value}
style={style}
/>

View File

@@ -136,6 +136,7 @@ export default function QBEntityOptions({
onChangeDataSource(value);
}
}}
data-testid={`query-data-source-selector-${index}`}
value={query?.dataSource || DataSource.METRICS}
isListViewPanel={isListViewPanel}
className="query-data-source-dropdown"

View File

@@ -27,6 +27,7 @@ export type MetricNameSelectorProps = {
defaultValue?: string;
onSelect?: (value: BaseAutocompleteData) => void;
signalSource?: 'meter' | '';
'data-testid'?: string;
};
function getAttributeType(
@@ -81,6 +82,7 @@ export const MetricNameSelector = memo(function MetricNameSelector({
defaultValue,
onSelect,
signalSource,
'data-testid': dataTestId,
}: MetricNameSelectorProps): JSX.Element {
const currentMetricName =
(query.aggregations?.[0] as MetricAggregation)?.metricName ||
@@ -279,6 +281,7 @@ export const MetricNameSelector = memo(function MetricNameSelector({
</Typography.Text>
) : null
}
data-testid={dataTestId}
options={optionsData}
value={inputValue}
onBlur={handleBlur}

View File

@@ -115,6 +115,7 @@ const useBaseAggregateOptions = ({
key={id}
icon={<LinkOutlined />}
onClick={(): void => {
// eslint-disable-next-line no-restricted-syntax -- context links can be internal or external URLs provided by users
window.open(url, '_blank');
onClose?.();
}}

View File

@@ -14,6 +14,7 @@ import { ModalTitle } from 'container/PipelinePage/PipelineListsView/styles';
import { Check, Loader, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';
import { openInNewTab } from 'utils/navigation';
import { INITIAL_ROUTING_POLICY_DETAILS_FORM_STATE } from './constants';
import {
@@ -76,7 +77,7 @@ function RoutingPolicyDetails({
style={{ padding: '0 4px' }}
type="link"
onClick={(): void => {
window.open(ROUTES.CHANNELS_NEW, '_blank');
openInNewTab(ROUTES.CHANNELS_NEW);
}}
>
here.

View File

@@ -64,7 +64,7 @@ import { USER_ROLES } from 'types/roles';
import { checkVersionState } from 'utils/app';
import { isModifierKeyPressed } from 'utils/app';
import { showErrorNotification } from 'utils/error';
import { openInNewTab } from 'utils/navigation';
import { openExternalLink, openInNewTab } from 'utils/navigation';
import signozBrandLogoUrl from '@/assets/Logos/signoz-brand-logo.svg';
@@ -818,7 +818,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
);
if (item && !('type' in item) && item.isExternal && item.url) {
window.open(item.url, '_blank');
openExternalLink(item.url);
}
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;

View File

@@ -28,6 +28,7 @@ import {
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuid } from 'uuid';
import noDataUrl from '@/assets/Icons/no-data.svg';
@@ -143,7 +144,7 @@ function SpanLogs({
const url = `${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`;
window.open(url, '_blank');
openInNewTab(url);
},
[
isLogSpanRelated,

View File

@@ -17,6 +17,7 @@ import { BarChart2, Compass, X } from 'lucide-react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Span } from 'types/api/trace/getTraceV2';
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
import { openInNewTab } from 'utils/navigation';
import { RelatedSignalsViews } from '../constants';
import SpanLogs from '../SpanLogs/SpanLogs';
@@ -157,13 +158,7 @@ function SpanRelatedSignals({
searchParams.set(QueryParams.startTime, startTimeMs.toString());
searchParams.set(QueryParams.endTime, endTimeMs.toString());
window.open(
`${window.location.origin}${
ROUTES.LOGS_EXPLORER
}?${searchParams.toString()}`,
'_blank',
'noopener,noreferrer',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${searchParams.toString()}`);
}, [selectedSpan.traceId, traceStartTime, traceEndTime]);
const emptyStateConfig = useMemo(

View File

@@ -31,6 +31,7 @@ import {
UPDATE_SPANS_AGGREGATE_PAGE_SIZE,
} from 'types/actions/trace';
import { TraceReducer } from 'types/reducer/trace';
import { openInNewTab } from 'utils/navigation';
import { v4 } from 'uuid';
dayjs.extend(duration);
@@ -214,7 +215,7 @@ function TraceTable(): JSX.Element {
event.preventDefault();
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
window.open(getLink(record), '_blank');
openInNewTab(getLink(record));
} else {
history.push(getLink(record));
}

View File

@@ -31,6 +31,7 @@ import {
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { Span } from 'types/api/trace/getTraceV2';
import { openExternalLink } from 'utils/navigation';
import { toFixed } from 'utils/toFixed';
import funnelAddUrl from '@/assets/Icons/funnel-add.svg';
@@ -547,12 +548,11 @@ function Success(props: ISuccessProps): JSX.Element {
icon={<ArrowUpRight size={14} />}
className="right-info"
type="text"
onClick={(): WindowProxy | null =>
window.open(
onClick={(): void => {
openExternalLink(
'https://signoz.io/docs/userguide/traces/#missing-spans',
'_blank',
)
}
);
}}
>
Learn More
</Button>

View File

@@ -28,6 +28,7 @@ import { useTimezone } from 'providers/Timezone';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { openInNewTab } from 'utils/navigation';
import './TracesTableComponent.styles.scss';
@@ -86,7 +87,7 @@ function TracesTableComponent({
event.preventDefault();
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
window.open(getTraceLink(record), '_blank');
openInNewTab(getTraceLink(record));
} else {
history.push(getTraceLink(record));
}

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