Compare commits
32 Commits
debug-wal
...
base-path-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d887b4a67e | ||
|
|
c88c7413ac | ||
|
|
622ccf6742 | ||
|
|
21821fde57 | ||
|
|
7208777ca6 | ||
|
|
ad3e0527ce | ||
|
|
555dc5881e | ||
|
|
4761fcb57a | ||
|
|
3df0da3a4e | ||
|
|
bda93ee286 | ||
|
|
7edc737076 | ||
|
|
381966adcd | ||
|
|
4fa306f94f | ||
|
|
c415b2a1d7 | ||
|
|
d513188511 | ||
|
|
e7edc5da82 | ||
|
|
dae7a43a52 | ||
|
|
9fb373e36b | ||
|
|
b01d4d8a74 | ||
|
|
df9023c74c | ||
|
|
10fc166e97 | ||
|
|
22b137c92b | ||
|
|
0b20afc469 | ||
|
|
dce496d099 | ||
|
|
c15e3ca59e | ||
|
|
9ec6da5e76 | ||
|
|
3e241944e7 | ||
|
|
5251339a16 | ||
|
|
702a16f80d | ||
|
|
7279c5f770 | ||
|
|
e543776efc | ||
|
|
621127b7fb |
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/SigNoz/signoz/cmd"
|
"github.com/SigNoz/signoz/cmd"
|
||||||
"github.com/SigNoz/signoz/pkg/analytics"
|
"github.com/SigNoz/signoz/pkg/analytics"
|
||||||
|
"github.com/SigNoz/signoz/pkg/auditor"
|
||||||
"github.com/SigNoz/signoz/pkg/authn"
|
"github.com/SigNoz/signoz/pkg/authn"
|
||||||
"github.com/SigNoz/signoz/pkg/authz"
|
"github.com/SigNoz/signoz/pkg/authz"
|
||||||
"github.com/SigNoz/signoz/pkg/authz/openfgaauthz"
|
"github.com/SigNoz/signoz/pkg/authz/openfgaauthz"
|
||||||
@@ -17,11 +18,15 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/gateway"
|
"github.com/SigNoz/signoz/pkg/gateway"
|
||||||
"github.com/SigNoz/signoz/pkg/gateway/noopgateway"
|
"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"
|
||||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
"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"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"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/querier"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/app"
|
"github.com/SigNoz/signoz/pkg/query-service/app"
|
||||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
"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] {
|
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
|
||||||
return noopgateway.NewProviderFactory()
|
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 {
|
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
|
||||||
return querier.NewHandler(ps, q, a)
|
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 {
|
if err != nil {
|
||||||
logger.ErrorContext(ctx, "failed to create signoz", errors.Attr(err))
|
logger.ErrorContext(ctx, "failed to create signoz", errors.Attr(err))
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/cmd"
|
"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/oidccallbackauthn"
|
||||||
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
|
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
|
||||||
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
|
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
|
||||||
@@ -16,6 +17,8 @@ import (
|
|||||||
"github.com/SigNoz/signoz/ee/gateway/httpgateway"
|
"github.com/SigNoz/signoz/ee/gateway/httpgateway"
|
||||||
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
|
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
|
||||||
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
"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"
|
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
|
||||||
eequerier "github.com/SigNoz/signoz/ee/querier"
|
eequerier "github.com/SigNoz/signoz/ee/querier"
|
||||||
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
|
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
|
||||||
@@ -24,15 +27,20 @@ import (
|
|||||||
enterprisezeus "github.com/SigNoz/signoz/ee/zeus"
|
enterprisezeus "github.com/SigNoz/signoz/ee/zeus"
|
||||||
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
|
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
|
||||||
"github.com/SigNoz/signoz/pkg/analytics"
|
"github.com/SigNoz/signoz/pkg/analytics"
|
||||||
|
"github.com/SigNoz/signoz/pkg/auditor"
|
||||||
"github.com/SigNoz/signoz/pkg/authn"
|
"github.com/SigNoz/signoz/pkg/authn"
|
||||||
"github.com/SigNoz/signoz/pkg/authz"
|
"github.com/SigNoz/signoz/pkg/authz"
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/gateway"
|
"github.com/SigNoz/signoz/pkg/gateway"
|
||||||
|
"github.com/SigNoz/signoz/pkg/global"
|
||||||
"github.com/SigNoz/signoz/pkg/licensing"
|
"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"
|
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||||
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"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/querier"
|
||||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||||
"github.com/SigNoz/signoz/pkg/signoz"
|
"github.com/SigNoz/signoz/pkg/signoz"
|
||||||
@@ -40,6 +48,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"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/version"
|
||||||
"github.com/SigNoz/signoz/pkg/zeus"
|
"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 nil, err
|
||||||
}
|
}
|
||||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, dashboardModule), nil
|
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 {
|
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)
|
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] {
|
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
|
||||||
return httpgateway.NewProviderFactory(licensing)
|
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 {
|
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
|
||||||
communityHandler := querier.NewHandler(ps, q, a)
|
communityHandler := querier.NewHandler(ps, q, a)
|
||||||
return eequerier.NewHandler(ps, q, communityHandler)
|
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 {
|
if err != nil {
|
||||||
logger.ErrorContext(ctx, "failed to create signoz", errors.Attr(err))
|
logger.ErrorContext(ctx, "failed to create signoz", errors.Attr(err))
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ sqlstore:
|
|||||||
provider: sqlite
|
provider: sqlite
|
||||||
# The maximum number of open connections to the database.
|
# The maximum number of open connections to the database.
|
||||||
max_open_conns: 100
|
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:
|
sqlite:
|
||||||
# The path to the SQLite database file.
|
# The path to the SQLite database file.
|
||||||
path: /var/lib/signoz/signoz.db
|
path: /var/lib/signoz/signoz.db
|
||||||
@@ -364,3 +367,41 @@ serviceaccount:
|
|||||||
analytics:
|
analytics:
|
||||||
# toggle service account analytics
|
# toggle service account analytics
|
||||||
enabled: true
|
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
|
||||||
|
|||||||
@@ -3309,7 +3309,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/CloudintegrationtypesPostableAccount'
|
$ref: '#/components/schemas/CloudintegrationtypesPostableAccount'
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"201":
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
@@ -3322,7 +3322,7 @@ paths:
|
|||||||
- status
|
- status
|
||||||
- data
|
- data
|
||||||
type: object
|
type: object
|
||||||
description: OK
|
description: Created
|
||||||
"401":
|
"401":
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
@@ -3683,6 +3683,11 @@ paths:
|
|||||||
provider
|
provider
|
||||||
operationId: ListServicesMetadata
|
operationId: ListServicesMetadata
|
||||||
parameters:
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: cloud_integration_id
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
- in: path
|
- in: path
|
||||||
name: cloud_provider
|
name: cloud_provider
|
||||||
required: true
|
required: true
|
||||||
@@ -3735,6 +3740,11 @@ paths:
|
|||||||
description: This endpoint gets a service for the specified cloud provider
|
description: This endpoint gets a service for the specified cloud provider
|
||||||
operationId: GetService
|
operationId: GetService
|
||||||
parameters:
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: cloud_integration_id
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
- in: path
|
- in: path
|
||||||
name: cloud_provider
|
name: cloud_provider
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
540
ee/modules/cloudintegration/implcloudintegration/module.go
Normal 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
|
||||||
|
}
|
||||||
@@ -7,6 +7,10 @@ import (
|
|||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
"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 {
|
type DayWiseBreakdown struct {
|
||||||
@@ -45,15 +49,17 @@ type details struct {
|
|||||||
BillTotal float64 `json:"billTotal"`
|
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 {
|
type billingDetails struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Data struct {
|
Data billingData `json:"data"`
|
||||||
BillingPeriodStart int64 `json:"billingPeriodStart"`
|
|
||||||
BillingPeriodEnd int64 `json:"billingPeriodEnd"`
|
|
||||||
Details details `json:"details"`
|
|
||||||
Discount float64 `json:"discount"`
|
|
||||||
SubscriptionStatus string `json:"subscriptionStatus"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
|
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
|
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)
|
billingURL := fmt.Sprintf("%s/usage?licenseKey=%s", constants.LicenseSignozIo, licenseKey)
|
||||||
|
|
||||||
hClient := &http.Client{}
|
hClient := &http.Client{}
|
||||||
@@ -79,13 +112,11 @@ func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode response body
|
|
||||||
var billingResponse billingDetails
|
var billingResponse billingDetails
|
||||||
if err := json.NewDecoder(billingResp.Body).Decode(&billingResponse); err != nil {
|
if err := json.NewDecoder(billingResp.Body).Decode(&billingResponse); err != nil {
|
||||||
RespondError(w, model.InternalError(err), nil)
|
RespondError(w, model.InternalError(err), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(srikanthccv):Fetch the current day usage and add it to the response
|
|
||||||
ah.Respond(w, billingResponse.Data)
|
ah.Respond(w, billingResponse.Data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
|||||||
s.config.APIServer.Timeout.Default,
|
s.config.APIServer.Timeout.Default,
|
||||||
s.config.APIServer.Timeout.Max,
|
s.config.APIServer.Timeout.Max,
|
||||||
).Wrap)
|
).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)
|
r.Use(middleware.NewComment().Wrap)
|
||||||
|
|
||||||
apiHandler.RegisterRoutes(r, am)
|
apiHandler.RegisterRoutes(r, am)
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
|
|||||||
|
|
||||||
// Set the maximum number of open connections
|
// Set the maximum number of open connections
|
||||||
pgConfig.MaxConns = int32(config.Connection.MaxOpenConns)
|
pgConfig.MaxConns = int32(config.Connection.MaxOpenConns)
|
||||||
|
pgConfig.MaxConnLifetime = config.Connection.MaxConnLifetime
|
||||||
|
|
||||||
// Use pgxpool to create a connection pool
|
// Use pgxpool to create a connection pool
|
||||||
pool, err := pgxpool.NewWithConfig(ctx, pgConfig)
|
pool, err := pgxpool.NewWithConfig(ctx, pgConfig)
|
||||||
|
|||||||
@@ -109,6 +109,21 @@ func (provider *Provider) GetDeployment(ctx context.Context, key string) ([]byte
|
|||||||
return []byte(gjson.GetBytes(response, "data").String()), nil
|
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 {
|
func (provider *Provider) PutMeters(ctx context.Context, key string, data []byte) error {
|
||||||
_, err := provider.do(
|
_, err := provider.do(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
|
eslint-rules/
|
||||||
|
stylelint-rules/
|
||||||
*.typegen.ts
|
*.typegen.ts
|
||||||
i18-generate-hash.js
|
i18-generate-hash.js
|
||||||
src/parser/TraceOperatorParser/**
|
src/parser/TraceOperatorParser/**
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const path = require('path');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const rulesDirPlugin = require('eslint-plugin-rulesdir');
|
||||||
|
// eslint-rules/ always points to frontend/eslint-rules/ regardless of workspace root.
|
||||||
|
rulesDirPlugin.RULES_DIR = path.join(__dirname, 'eslint-rules');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ESLint Configuration for SigNoz Frontend
|
* ESLint Configuration for SigNoz Frontend
|
||||||
*/
|
*/
|
||||||
@@ -32,6 +39,7 @@ module.exports = {
|
|||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
'rulesdir', // Local custom rules
|
||||||
'react', // React-specific rules
|
'react', // React-specific rules
|
||||||
'@typescript-eslint', // TypeScript linting
|
'@typescript-eslint', // TypeScript linting
|
||||||
'simple-import-sort', // Auto-sort imports
|
'simple-import-sort', // Auto-sort imports
|
||||||
@@ -56,6 +64,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
// Asset migration — base-path safety
|
||||||
|
'rulesdir/no-unsupported-asset-pattern': 'error',
|
||||||
|
|
||||||
// Code quality rules
|
// Code quality rules
|
||||||
'prefer-const': 'error', // Enforces const for variables never reassigned
|
'prefer-const': 'error', // Enforces const for variables never reassigned
|
||||||
'no-var': 'error', // Disallows var, enforces let/const
|
'no-var': 'error', // Disallows var, enforces let/const
|
||||||
@@ -202,6 +213,23 @@ module.exports = {
|
|||||||
message:
|
message:
|
||||||
'Avoid calling .getState() directly. Export a standalone action from the store instead.',
|
'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: [
|
overrides: [
|
||||||
@@ -254,5 +282,14 @@ module.exports = {
|
|||||||
'no-restricted-syntax': 'off',
|
'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',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
9
frontend/.stylelintrc.cjs
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
1
frontend/__mocks__/fileMock.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export default 'test-file-stub';
|
||||||
390
frontend/eslint-rules/no-unsupported-asset-pattern.js
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ESLint rule: no-unsupported-asset-pattern
|
||||||
|
*
|
||||||
|
* Enforces that all asset references (SVG, PNG, etc.) go through Vite's module
|
||||||
|
* pipeline via ES imports (`import fooUrl from '@/assets/...'`) rather than
|
||||||
|
* hard-coded strings or public/ paths.
|
||||||
|
*
|
||||||
|
* Why this matters: when the app is served from a sub-path (base-path deployment),
|
||||||
|
* Vite rewrites ES import URLs automatically. String literals and public/ references
|
||||||
|
* bypass that rewrite and break at runtime.
|
||||||
|
*
|
||||||
|
* Covers four AST patterns:
|
||||||
|
* 1. Literal — plain string: "/Icons/logo.svg"
|
||||||
|
* 2. TemplateLiteral — template string: `/Icons/${name}.svg`
|
||||||
|
* 3. BinaryExpression — concatenation: "/Icons/" + name + ".svg"
|
||||||
|
* 4. ImportDeclaration / ImportExpression — static & dynamic imports
|
||||||
|
*/
|
||||||
|
|
||||||
|
const {
|
||||||
|
hasAssetExtension,
|
||||||
|
containsAssetExtension,
|
||||||
|
extractUrlPath,
|
||||||
|
isAbsolutePath,
|
||||||
|
isPublicRelative,
|
||||||
|
isRelativePublicDir,
|
||||||
|
isValidAssetImport,
|
||||||
|
isExternalUrl,
|
||||||
|
} = require('./shared/asset-patterns');
|
||||||
|
|
||||||
|
// Known public/ sub-directories that should never appear in dynamic asset paths.
|
||||||
|
const PUBLIC_DIR_SEGMENTS = ['/Icons/', '/Images/', '/Logos/', '/svgs/'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively extracts the static string parts from a binary `+` expression or
|
||||||
|
* template literal. Returns `[null]` for any dynamic (non-string) node so
|
||||||
|
* callers can detect that the prefix became unknowable.
|
||||||
|
*
|
||||||
|
* Example: `"/Icons/" + iconName + ".svg"` → ["/Icons/", null, ".svg"]
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
// Unknown / dynamic node — signals "prefix is no longer fully static"
|
||||||
|
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 {
|
||||||
|
/**
|
||||||
|
* Catches plain string literals used as asset paths, e.g.:
|
||||||
|
* src="/Icons/logo.svg" or url("../public/Images/bg.png")
|
||||||
|
*
|
||||||
|
* Import declaration sources are skipped here — handled by ImportDeclaration.
|
||||||
|
* Also unwraps CSS `url(...)` wrappers before checking.
|
||||||
|
*/
|
||||||
|
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) && containsAssetExtension(value)) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'absoluteString',
|
||||||
|
data: { value },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRelativePublicDir(value) && containsAssetExtension(value)) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'relativePublicString',
|
||||||
|
data: { value },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catches relative paths that start with "public/" e.g. 'public/Logos/aws-dark.svg'.
|
||||||
|
// isRelativePublicDir only covers known sub-dirs (Icons/, Logos/, etc.),
|
||||||
|
// so this handles the case where the full "public/" prefix is written explicitly.
|
||||||
|
if (isPublicRelative(value) && containsAssetExtension(value)) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'relativePublicString',
|
||||||
|
data: { value },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check the path inside a CSS url("...") wrapper
|
||||||
|
const urlPath = extractUrlPath(value);
|
||||||
|
if (urlPath && isExternalUrl(urlPath)) return;
|
||||||
|
if (urlPath && isAbsolutePath(urlPath) && containsAssetExtension(urlPath)) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'absoluteString',
|
||||||
|
data: { value: urlPath },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
urlPath &&
|
||||||
|
isRelativePublicDir(urlPath) &&
|
||||||
|
containsAssetExtension(urlPath)
|
||||||
|
) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'relativePublicString',
|
||||||
|
data: { value: urlPath },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
urlPath &&
|
||||||
|
isPublicRelative(urlPath) &&
|
||||||
|
containsAssetExtension(urlPath)
|
||||||
|
) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'relativePublicString',
|
||||||
|
data: { value: urlPath },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catches template literals used as asset paths, e.g.:
|
||||||
|
* `/Icons/${name}.svg`
|
||||||
|
* `url('/Images/${bg}.png')`
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No-interpolation template (single quasi): treat like a plain string
|
||||||
|
// and also unwrap any css url(...) wrapper.
|
||||||
|
if (quasis.length === 1) {
|
||||||
|
// Check the raw string first (no url() wrapper)
|
||||||
|
if (isPublicRelative(firstQuasi) && hasAssetExt) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'relativePublicString',
|
||||||
|
data: { value: firstQuasi },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (urlPath && isPublicRelative(urlPath) && hasAssetExtension(urlPath)) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'relativePublicString',
|
||||||
|
data: { value: urlPath },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS url() with an absolute path inside a multi-quasi template, e.g.:
|
||||||
|
// `url('/Icons/${name}.svg')`
|
||||||
|
if (firstQuasi.includes('url(') && hasAssetExt) {
|
||||||
|
const urlMatch = firstQuasi.match(/^url\(\s*['"]?\//);
|
||||||
|
if (urlMatch) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'templateLiteral',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catches string concatenation used to build asset paths, e.g.:
|
||||||
|
* "/Icons/" + name + ".svg"
|
||||||
|
*
|
||||||
|
* Collects the leading static parts (before the first dynamic value)
|
||||||
|
* to determine the path prefix. If any part carries a known asset
|
||||||
|
* extension, the expression is flagged.
|
||||||
|
*/
|
||||||
|
BinaryExpression(node) {
|
||||||
|
if (node.operator !== '+') return;
|
||||||
|
|
||||||
|
const parts = collectBinaryStringParts(node);
|
||||||
|
// Collect only the leading static parts; stop at the first dynamic (null) part
|
||||||
|
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 (isPublicRelative(staticPrefix) && hasExt) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'relativePublicString',
|
||||||
|
data: { value: staticPrefix },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRelativePublicDir(staticPrefix) && hasExt) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'relativePublicString',
|
||||||
|
data: { value: staticPrefix },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catches static asset imports that don't go through src/assets/, e.g.:
|
||||||
|
* import logo from '/public/Icons/logo.svg' ← absolute path
|
||||||
|
* import logo from '../../public/logo.svg' ← relative into public/
|
||||||
|
* import logo from '../somewhere/logo.svg' ← outside src/assets/
|
||||||
|
*
|
||||||
|
* Valid pattern: import fooUrl from '@/assets/...' or relative within src/assets/
|
||||||
|
*/
|
||||||
|
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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same checks as ImportDeclaration but for dynamic imports:
|
||||||
|
* const logo = await import('/Icons/logo.svg')
|
||||||
|
*
|
||||||
|
* Only literal sources are checked; fully dynamic expressions are ignored
|
||||||
|
* since their paths cannot be statically analysed.
|
||||||
|
*/
|
||||||
|
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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
3
frontend/eslint-rules/package.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
121
frontend/eslint-rules/shared/asset-patterns.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const ALLOWED_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 ALLOWED_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 ALLOWED_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 = {
|
||||||
|
ALLOWED_ASSET_EXTENSIONS,
|
||||||
|
PUBLIC_DIR_SEGMENTS,
|
||||||
|
hasAssetExtension,
|
||||||
|
containsAssetExtension,
|
||||||
|
extractUrlPath,
|
||||||
|
isAbsolutePath,
|
||||||
|
isPublicRelative,
|
||||||
|
isRelativePublicDir,
|
||||||
|
isValidAssetImport,
|
||||||
|
isExternalUrl,
|
||||||
|
};
|
||||||
@@ -6,3 +6,6 @@ VITE_APPCUES_APP_ID="appcess-app-id"
|
|||||||
VITE_PYLON_IDENTITY_SECRET="pylon-identity-secret"
|
VITE_PYLON_IDENTITY_SECRET="pylon-identity-secret"
|
||||||
|
|
||||||
CI="1"
|
CI="1"
|
||||||
|
|
||||||
|
# Uncomment to test sub-path deployment locally (e.g. app served at /signoz/).
|
||||||
|
# VITE_BASE_PATH="/signoz/"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<base href="<%- BASE_PATH %>" />
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Cache-Control"
|
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_locale" content="en" />
|
||||||
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
|
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
|
||||||
<meta name="robots" content="noindex" />
|
<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>
|
</head>
|
||||||
<body data-theme="default">
|
<body data-theme="default">
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
@@ -113,7 +114,7 @@
|
|||||||
})(document, 'script');
|
})(document, 'script');
|
||||||
}
|
}
|
||||||
</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>
|
<script type="module" src="./src/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -11,12 +11,16 @@ const config: Config.InitialOptions = {
|
|||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
|
||||||
modulePathIgnorePatterns: ['dist'],
|
modulePathIgnorePatterns: ['dist'],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
|
'\\.(png|jpg|jpeg|gif|svg|webp|avif|ico|bmp|tiff)$':
|
||||||
|
'<rootDir>/__mocks__/fileMock.ts',
|
||||||
|
'^@/(.*)$': '<rootDir>/src/$1',
|
||||||
'\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts',
|
'\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts',
|
||||||
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
|
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
|
||||||
'^uplot$': '<rootDir>/__mocks__/uplotMock.ts',
|
'^uplot$': '<rootDir>/__mocks__/uplotMock.ts',
|
||||||
'^@signozhq/resizable$': '<rootDir>/__mocks__/resizableMock.tsx',
|
'^@signozhq/resizable$': '<rootDir>/__mocks__/resizableMock.tsx',
|
||||||
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||||
'^src/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,
|
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||||
'^constants/env$': '<rootDir>/__mocks__/env.ts',
|
'^constants/env$': '<rootDir>/__mocks__/env.ts',
|
||||||
'^src/constants/env$': '<rootDir>/__mocks__/env.ts',
|
'^src/constants/env$': '<rootDir>/__mocks__/env.ts',
|
||||||
|
|||||||
@@ -10,9 +10,10 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"prettify": "prettier --write .",
|
"prettify": "prettier --write .",
|
||||||
"fmt": "prettier --check .",
|
"fmt": "prettier --check .",
|
||||||
"lint": "eslint ./src",
|
"lint": "eslint ./src && stylelint \"src/**/*.scss\"",
|
||||||
"lint:generated": "eslint ./src/api/generated --fix",
|
"lint:generated": "eslint ./src/api/generated --fix",
|
||||||
"lint:fix": "eslint ./src --fix",
|
"lint:fix": "eslint ./src --fix",
|
||||||
|
"lint:styles": "stylelint \"src/**/*.scss\"",
|
||||||
"jest": "jest",
|
"jest": "jest",
|
||||||
"jest:coverage": "jest --coverage",
|
"jest:coverage": "jest --coverage",
|
||||||
"jest:watch": "jest --watch",
|
"jest:watch": "jest --watch",
|
||||||
@@ -229,6 +230,7 @@
|
|||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-plugin-react": "^7.24.0",
|
"eslint-plugin-react": "^7.24.0",
|
||||||
"eslint-plugin-react-hooks": "^4.3.0",
|
"eslint-plugin-react-hooks": "^4.3.0",
|
||||||
|
"eslint-plugin-rulesdir": "0.2.2",
|
||||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||||
"eslint-plugin-sonarjs": "^0.12.0",
|
"eslint-plugin-sonarjs": "^0.12.0",
|
||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
@@ -244,6 +246,7 @@
|
|||||||
"orval": "7.18.0",
|
"orval": "7.18.0",
|
||||||
"portfinder-sync": "^0.0.2",
|
"portfinder-sync": "^0.0.2",
|
||||||
"postcss": "8.5.6",
|
"postcss": "8.5.6",
|
||||||
|
"postcss-scss": "4.0.9",
|
||||||
"prettier": "2.2.1",
|
"prettier": "2.2.1",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"react-hooks-testing-library": "0.6.0",
|
"react-hooks-testing-library": "0.6.0",
|
||||||
@@ -251,6 +254,8 @@
|
|||||||
"redux-mock-store": "1.5.4",
|
"redux-mock-store": "1.5.4",
|
||||||
"sass": "1.97.3",
|
"sass": "1.97.3",
|
||||||
"sharp": "0.34.5",
|
"sharp": "0.34.5",
|
||||||
|
"stylelint": "17.7.0",
|
||||||
|
"stylelint-scss": "7.0.0",
|
||||||
"svgo": "4.0.0",
|
"svgo": "4.0.0",
|
||||||
"ts-api-utils": "2.4.0",
|
"ts-api-utils": "2.4.0",
|
||||||
"ts-jest": "29.4.6",
|
"ts-jest": "29.4.6",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import type {
|
|||||||
CloudintegrationtypesPostableAgentCheckInDTO,
|
CloudintegrationtypesPostableAgentCheckInDTO,
|
||||||
CloudintegrationtypesUpdatableAccountDTO,
|
CloudintegrationtypesUpdatableAccountDTO,
|
||||||
CloudintegrationtypesUpdatableServiceDTO,
|
CloudintegrationtypesUpdatableServiceDTO,
|
||||||
CreateAccount200,
|
CreateAccount201,
|
||||||
CreateAccountPathParameters,
|
CreateAccountPathParameters,
|
||||||
DisconnectAccountPathParameters,
|
DisconnectAccountPathParameters,
|
||||||
GetAccount200,
|
GetAccount200,
|
||||||
@@ -36,10 +36,12 @@ import type {
|
|||||||
GetConnectionCredentials200,
|
GetConnectionCredentials200,
|
||||||
GetConnectionCredentialsPathParameters,
|
GetConnectionCredentialsPathParameters,
|
||||||
GetService200,
|
GetService200,
|
||||||
|
GetServiceParams,
|
||||||
GetServicePathParameters,
|
GetServicePathParameters,
|
||||||
ListAccounts200,
|
ListAccounts200,
|
||||||
ListAccountsPathParameters,
|
ListAccountsPathParameters,
|
||||||
ListServicesMetadata200,
|
ListServicesMetadata200,
|
||||||
|
ListServicesMetadataParams,
|
||||||
ListServicesMetadataPathParameters,
|
ListServicesMetadataPathParameters,
|
||||||
RenderErrorResponseDTO,
|
RenderErrorResponseDTO,
|
||||||
UpdateAccountPathParameters,
|
UpdateAccountPathParameters,
|
||||||
@@ -260,7 +262,7 @@ export const createAccount = (
|
|||||||
cloudintegrationtypesPostableAccountDTO: BodyType<CloudintegrationtypesPostableAccountDTO>,
|
cloudintegrationtypesPostableAccountDTO: BodyType<CloudintegrationtypesPostableAccountDTO>,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
) => {
|
) => {
|
||||||
return GeneratedAPIInstance<CreateAccount200>({
|
return GeneratedAPIInstance<CreateAccount201>({
|
||||||
url: `/api/v1/cloud_integrations/${cloudProvider}/accounts`,
|
url: `/api/v1/cloud_integrations/${cloudProvider}/accounts`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -940,19 +942,25 @@ export const invalidateGetConnectionCredentials = async (
|
|||||||
*/
|
*/
|
||||||
export const listServicesMetadata = (
|
export const listServicesMetadata = (
|
||||||
{ cloudProvider }: ListServicesMetadataPathParameters,
|
{ cloudProvider }: ListServicesMetadataPathParameters,
|
||||||
|
params?: ListServicesMetadataParams,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
) => {
|
) => {
|
||||||
return GeneratedAPIInstance<ListServicesMetadata200>({
|
return GeneratedAPIInstance<ListServicesMetadata200>({
|
||||||
url: `/api/v1/cloud_integrations/${cloudProvider}/services`,
|
url: `/api/v1/cloud_integrations/${cloudProvider}/services`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
params,
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getListServicesMetadataQueryKey = ({
|
export const getListServicesMetadataQueryKey = (
|
||||||
cloudProvider,
|
{ cloudProvider }: ListServicesMetadataPathParameters,
|
||||||
}: ListServicesMetadataPathParameters) => {
|
params?: ListServicesMetadataParams,
|
||||||
return [`/api/v1/cloud_integrations/${cloudProvider}/services`] as const;
|
) => {
|
||||||
|
return [
|
||||||
|
`/api/v1/cloud_integrations/${cloudProvider}/services`,
|
||||||
|
...(params ? [params] : []),
|
||||||
|
] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getListServicesMetadataQueryOptions = <
|
export const getListServicesMetadataQueryOptions = <
|
||||||
@@ -960,6 +968,7 @@ export const getListServicesMetadataQueryOptions = <
|
|||||||
TError = ErrorType<RenderErrorResponseDTO>
|
TError = ErrorType<RenderErrorResponseDTO>
|
||||||
>(
|
>(
|
||||||
{ cloudProvider }: ListServicesMetadataPathParameters,
|
{ cloudProvider }: ListServicesMetadataPathParameters,
|
||||||
|
params?: ListServicesMetadataParams,
|
||||||
options?: {
|
options?: {
|
||||||
query?: UseQueryOptions<
|
query?: UseQueryOptions<
|
||||||
Awaited<ReturnType<typeof listServicesMetadata>>,
|
Awaited<ReturnType<typeof listServicesMetadata>>,
|
||||||
@@ -971,11 +980,12 @@ export const getListServicesMetadataQueryOptions = <
|
|||||||
const { query: queryOptions } = options ?? {};
|
const { query: queryOptions } = options ?? {};
|
||||||
|
|
||||||
const queryKey =
|
const queryKey =
|
||||||
queryOptions?.queryKey ?? getListServicesMetadataQueryKey({ cloudProvider });
|
queryOptions?.queryKey ??
|
||||||
|
getListServicesMetadataQueryKey({ cloudProvider }, params);
|
||||||
|
|
||||||
const queryFn: QueryFunction<
|
const queryFn: QueryFunction<
|
||||||
Awaited<ReturnType<typeof listServicesMetadata>>
|
Awaited<ReturnType<typeof listServicesMetadata>>
|
||||||
> = ({ signal }) => listServicesMetadata({ cloudProvider }, signal);
|
> = ({ signal }) => listServicesMetadata({ cloudProvider }, params, signal);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
queryKey,
|
queryKey,
|
||||||
@@ -1003,6 +1013,7 @@ export function useListServicesMetadata<
|
|||||||
TError = ErrorType<RenderErrorResponseDTO>
|
TError = ErrorType<RenderErrorResponseDTO>
|
||||||
>(
|
>(
|
||||||
{ cloudProvider }: ListServicesMetadataPathParameters,
|
{ cloudProvider }: ListServicesMetadataPathParameters,
|
||||||
|
params?: ListServicesMetadataParams,
|
||||||
options?: {
|
options?: {
|
||||||
query?: UseQueryOptions<
|
query?: UseQueryOptions<
|
||||||
Awaited<ReturnType<typeof listServicesMetadata>>,
|
Awaited<ReturnType<typeof listServicesMetadata>>,
|
||||||
@@ -1013,6 +1024,7 @@ export function useListServicesMetadata<
|
|||||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||||
const queryOptions = getListServicesMetadataQueryOptions(
|
const queryOptions = getListServicesMetadataQueryOptions(
|
||||||
{ cloudProvider },
|
{ cloudProvider },
|
||||||
|
params,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1031,10 +1043,11 @@ export function useListServicesMetadata<
|
|||||||
export const invalidateListServicesMetadata = async (
|
export const invalidateListServicesMetadata = async (
|
||||||
queryClient: QueryClient,
|
queryClient: QueryClient,
|
||||||
{ cloudProvider }: ListServicesMetadataPathParameters,
|
{ cloudProvider }: ListServicesMetadataPathParameters,
|
||||||
|
params?: ListServicesMetadataParams,
|
||||||
options?: InvalidateOptions,
|
options?: InvalidateOptions,
|
||||||
): Promise<QueryClient> => {
|
): Promise<QueryClient> => {
|
||||||
await queryClient.invalidateQueries(
|
await queryClient.invalidateQueries(
|
||||||
{ queryKey: getListServicesMetadataQueryKey({ cloudProvider }) },
|
{ queryKey: getListServicesMetadataQueryKey({ cloudProvider }, params) },
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1047,21 +1060,24 @@ export const invalidateListServicesMetadata = async (
|
|||||||
*/
|
*/
|
||||||
export const getService = (
|
export const getService = (
|
||||||
{ cloudProvider, serviceId }: GetServicePathParameters,
|
{ cloudProvider, serviceId }: GetServicePathParameters,
|
||||||
|
params?: GetServiceParams,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
) => {
|
) => {
|
||||||
return GeneratedAPIInstance<GetService200>({
|
return GeneratedAPIInstance<GetService200>({
|
||||||
url: `/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
|
url: `/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
params,
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGetServiceQueryKey = ({
|
export const getGetServiceQueryKey = (
|
||||||
cloudProvider,
|
{ cloudProvider, serviceId }: GetServicePathParameters,
|
||||||
serviceId,
|
params?: GetServiceParams,
|
||||||
}: GetServicePathParameters) => {
|
) => {
|
||||||
return [
|
return [
|
||||||
`/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
|
`/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
|
||||||
|
...(params ? [params] : []),
|
||||||
] as const;
|
] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1070,6 +1086,7 @@ export const getGetServiceQueryOptions = <
|
|||||||
TError = ErrorType<RenderErrorResponseDTO>
|
TError = ErrorType<RenderErrorResponseDTO>
|
||||||
>(
|
>(
|
||||||
{ cloudProvider, serviceId }: GetServicePathParameters,
|
{ cloudProvider, serviceId }: GetServicePathParameters,
|
||||||
|
params?: GetServiceParams,
|
||||||
options?: {
|
options?: {
|
||||||
query?: UseQueryOptions<
|
query?: UseQueryOptions<
|
||||||
Awaited<ReturnType<typeof getService>>,
|
Awaited<ReturnType<typeof getService>>,
|
||||||
@@ -1081,11 +1098,12 @@ export const getGetServiceQueryOptions = <
|
|||||||
const { query: queryOptions } = options ?? {};
|
const { query: queryOptions } = options ?? {};
|
||||||
|
|
||||||
const queryKey =
|
const queryKey =
|
||||||
queryOptions?.queryKey ?? getGetServiceQueryKey({ cloudProvider, serviceId });
|
queryOptions?.queryKey ??
|
||||||
|
getGetServiceQueryKey({ cloudProvider, serviceId }, params);
|
||||||
|
|
||||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getService>>> = ({
|
const queryFn: QueryFunction<Awaited<ReturnType<typeof getService>>> = ({
|
||||||
signal,
|
signal,
|
||||||
}) => getService({ cloudProvider, serviceId }, signal);
|
}) => getService({ cloudProvider, serviceId }, params, signal);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
queryKey,
|
queryKey,
|
||||||
@@ -1111,6 +1129,7 @@ export function useGetService<
|
|||||||
TError = ErrorType<RenderErrorResponseDTO>
|
TError = ErrorType<RenderErrorResponseDTO>
|
||||||
>(
|
>(
|
||||||
{ cloudProvider, serviceId }: GetServicePathParameters,
|
{ cloudProvider, serviceId }: GetServicePathParameters,
|
||||||
|
params?: GetServiceParams,
|
||||||
options?: {
|
options?: {
|
||||||
query?: UseQueryOptions<
|
query?: UseQueryOptions<
|
||||||
Awaited<ReturnType<typeof getService>>,
|
Awaited<ReturnType<typeof getService>>,
|
||||||
@@ -1121,6 +1140,7 @@ export function useGetService<
|
|||||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||||
const queryOptions = getGetServiceQueryOptions(
|
const queryOptions = getGetServiceQueryOptions(
|
||||||
{ cloudProvider, serviceId },
|
{ cloudProvider, serviceId },
|
||||||
|
params,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1139,10 +1159,11 @@ export function useGetService<
|
|||||||
export const invalidateGetService = async (
|
export const invalidateGetService = async (
|
||||||
queryClient: QueryClient,
|
queryClient: QueryClient,
|
||||||
{ cloudProvider, serviceId }: GetServicePathParameters,
|
{ cloudProvider, serviceId }: GetServicePathParameters,
|
||||||
|
params?: GetServiceParams,
|
||||||
options?: InvalidateOptions,
|
options?: InvalidateOptions,
|
||||||
): Promise<QueryClient> => {
|
): Promise<QueryClient> => {
|
||||||
await queryClient.invalidateQueries(
|
await queryClient.invalidateQueries(
|
||||||
{ queryKey: getGetServiceQueryKey({ cloudProvider, serviceId }) },
|
{ queryKey: getGetServiceQueryKey({ cloudProvider, serviceId }, params) },
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -3589,7 +3589,7 @@ export type ListAccounts200 = {
|
|||||||
export type CreateAccountPathParameters = {
|
export type CreateAccountPathParameters = {
|
||||||
cloudProvider: string;
|
cloudProvider: string;
|
||||||
};
|
};
|
||||||
export type CreateAccount200 = {
|
export type CreateAccount201 = {
|
||||||
data: CloudintegrationtypesGettableAccountWithConnectionArtifactDTO;
|
data: CloudintegrationtypesGettableAccountWithConnectionArtifactDTO;
|
||||||
/**
|
/**
|
||||||
* @type string
|
* @type string
|
||||||
@@ -3647,6 +3647,14 @@ export type GetConnectionCredentials200 = {
|
|||||||
export type ListServicesMetadataPathParameters = {
|
export type ListServicesMetadataPathParameters = {
|
||||||
cloudProvider: string;
|
cloudProvider: string;
|
||||||
};
|
};
|
||||||
|
export type ListServicesMetadataParams = {
|
||||||
|
/**
|
||||||
|
* @type string
|
||||||
|
* @description undefined
|
||||||
|
*/
|
||||||
|
cloud_integration_id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ListServicesMetadata200 = {
|
export type ListServicesMetadata200 = {
|
||||||
data: CloudintegrationtypesGettableServicesMetadataDTO;
|
data: CloudintegrationtypesGettableServicesMetadataDTO;
|
||||||
/**
|
/**
|
||||||
@@ -3659,6 +3667,14 @@ export type GetServicePathParameters = {
|
|||||||
cloudProvider: string;
|
cloudProvider: string;
|
||||||
serviceId: string;
|
serviceId: string;
|
||||||
};
|
};
|
||||||
|
export type GetServiceParams = {
|
||||||
|
/**
|
||||||
|
* @type string
|
||||||
|
* @description undefined
|
||||||
|
*/
|
||||||
|
cloud_integration_id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type GetService200 = {
|
export type GetService200 = {
|
||||||
data: CloudintegrationtypesServiceDTO;
|
data: CloudintegrationtypesServiceDTO;
|
||||||
/**
|
/**
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 507 B After Width: | Height: | Size: 507 B |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 644 B After Width: | Height: | Size: 644 B |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 418 B After Width: | Height: | Size: 418 B |
|
Before Width: | Height: | Size: 604 B After Width: | Height: | Size: 604 B |
|
Before Width: | Height: | Size: 878 B After Width: | Height: | Size: 878 B |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 303 B After Width: | Height: | Size: 303 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 629 B |
|
Before Width: | Height: | Size: 439 B After Width: | Height: | Size: 439 B |
|
Before Width: | Height: | Size: 467 B After Width: | Height: | Size: 467 B |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 300 B After Width: | Height: | Size: 300 B |
|
Before Width: | Height: | Size: 348 B After Width: | Height: | Size: 348 B |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 910 B After Width: | Height: | Size: 910 B |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 204 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 408 KiB After Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 695 B After Width: | Height: | Size: 695 B |
|
Before Width: | Height: | Size: 714 B After Width: | Height: | Size: 714 B |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 872 B After Width: | Height: | Size: 872 B |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |