mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-27 18:52:53 +00:00
Compare commits
8 Commits
members-pa
...
nitya/clou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9d33e83a2 | ||
|
|
afcf2c6053 | ||
|
|
ec59fecdda | ||
|
|
5a762c678e | ||
|
|
55b1311f78 | ||
|
|
59668698a2 | ||
|
|
22ed687d44 | ||
|
|
1da016cf1a |
@@ -320,4 +320,3 @@ user:
|
||||
# The name of the organization to create or look up for the root user.
|
||||
org:
|
||||
name: default
|
||||
id: 00000000-0000-0000-0000-000000000000
|
||||
|
||||
@@ -842,17 +842,6 @@ components:
|
||||
- temporality
|
||||
- isMonotonic
|
||||
type: object
|
||||
MetrictypesComparisonSpaceAggregationParam:
|
||||
properties:
|
||||
operator:
|
||||
type: string
|
||||
threshold:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- operator
|
||||
- threshold
|
||||
type: object
|
||||
MetrictypesSpaceAggregation:
|
||||
enum:
|
||||
- sum
|
||||
@@ -1149,8 +1138,6 @@ components:
|
||||
type: object
|
||||
Querybuildertypesv5MetricAggregation:
|
||||
properties:
|
||||
comparisonSpaceAggregationParam:
|
||||
$ref: '#/components/schemas/MetrictypesComparisonSpaceAggregationParam'
|
||||
metricName:
|
||||
type: string
|
||||
reduceTo:
|
||||
|
||||
164
ee/modules/cloudintegrations/implawsprovider/provider.go
Normal file
164
ee/modules/cloudintegrations/implawsprovider/provider.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package implawsprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegrations"
|
||||
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
|
||||
)
|
||||
|
||||
var _ cloudintegrations.CloudProvider = (*AWSProvider)(nil)
|
||||
|
||||
type AWSProvider struct {
|
||||
store integrationtypes.Store
|
||||
}
|
||||
|
||||
func NewAWSProvider(store integrationtypes.Store) *AWSProvider {
|
||||
return &AWSProvider{store: store}
|
||||
}
|
||||
|
||||
func (a *AWSProvider) AgentCheckIn(ctx context.Context, req *cloudintegrations.PostableAgentCheckInPayload) (any, error) {
|
||||
// if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
||||
// return nil, apiErr
|
||||
// }
|
||||
|
||||
// existingAccount, apiErr := c.accountsRepo.get(ctx, orgId, cloudProvider, req.ID)
|
||||
// if existingAccount != nil && existingAccount.AccountID != nil && *existingAccount.AccountID != req.AccountID {
|
||||
// return nil, model.BadRequest(fmt.Errorf(
|
||||
// "can't check in with new %s account id %s for account %s with existing %s id %s",
|
||||
// cloudProvider, req.AccountID, existingAccount.ID.StringValue(), cloudProvider, *existingAccount.AccountID,
|
||||
// ))
|
||||
// }
|
||||
|
||||
// existingAccount, apiErr = c.accountsRepo.getConnectedCloudAccount(ctx, orgId, cloudProvider, req.AccountID)
|
||||
// if existingAccount != nil && existingAccount.ID.StringValue() != req.ID {
|
||||
// return nil, model.BadRequest(fmt.Errorf(
|
||||
// "can't check in to %s account %s with id %s. already connected with id %s",
|
||||
// cloudProvider, req.AccountID, req.ID, existingAccount.ID.StringValue(),
|
||||
// ))
|
||||
// }
|
||||
|
||||
// agentReport := types.AgentReport{
|
||||
// TimestampMillis: time.Now().UnixMilli(),
|
||||
// Data: req.Data,
|
||||
// }
|
||||
|
||||
// account, apiErr := c.accountsRepo.upsert(
|
||||
// ctx, orgId, cloudProvider, &req.ID, nil, &req.AccountID, &agentReport, nil,
|
||||
// )
|
||||
// if apiErr != nil {
|
||||
// return nil, model.WrapApiError(apiErr, "couldn't upsert cloud account")
|
||||
// }
|
||||
|
||||
// // prepare and return integration config to be consumed by agent
|
||||
// compiledStrategy, err := NewCompiledCollectionStrategy(cloudProvider)
|
||||
// if err != nil {
|
||||
// return nil, model.InternalError(fmt.Errorf(
|
||||
// "couldn't init telemetry collection strategy: %w", err,
|
||||
// ))
|
||||
// }
|
||||
|
||||
// agentConfig := IntegrationConfigForAgent{
|
||||
// EnabledRegions: []string{},
|
||||
// TelemetryCollectionStrategy: compiledStrategy,
|
||||
// }
|
||||
|
||||
// if account.Config != nil && account.Config.EnabledRegions != nil {
|
||||
// agentConfig.EnabledRegions = account.Config.EnabledRegions
|
||||
// }
|
||||
|
||||
// services, err := services.Map(cloudProvider)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// svcConfigs, apiErr := c.serviceConfigRepo.getAllForAccount(
|
||||
// ctx, orgId, account.ID.StringValue(),
|
||||
// )
|
||||
// if apiErr != nil {
|
||||
// return nil, model.WrapApiError(
|
||||
// apiErr, "couldn't get service configs for cloud account",
|
||||
// )
|
||||
// }
|
||||
|
||||
// // accumulate config in a fixed order to ensure same config generated across runs
|
||||
// configuredServices := maps.Keys(svcConfigs)
|
||||
// slices.Sort(configuredServices)
|
||||
|
||||
// for _, svcType := range configuredServices {
|
||||
// definition, ok := services[svcType]
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
// config := svcConfigs[svcType]
|
||||
|
||||
// err := AddServiceStrategy(svcType, compiledStrategy, definition.Strategy, config)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// }
|
||||
|
||||
// return &AgentCheckInResponse{
|
||||
// AccountId: account.ID.StringValue(),
|
||||
// CloudAccountId: *account.AccountID,
|
||||
// RemovedAt: account.RemovedAt,
|
||||
// IntegrationConfig: agentConfig,
|
||||
// }, nil
|
||||
}
|
||||
|
||||
func (a *AWSProvider) ListServices(ctx context.Context, orgID string, cloudAccountID *string) (any, error) {
|
||||
svcConfigs := make(map[string]*integrationtypes.AWSServiceConfig)
|
||||
if cloudAccountID != nil {
|
||||
activeAccount, err := a.store.GetConnectedCloudAccount(ctx, orgID, a.GetName().String(), *cloudAccountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceConfigs, err := a.ServiceConfigRepo.GetAllForAccount(ctx, orgID, activeAccount.ID.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for svcType, config := range serviceConfigs {
|
||||
serviceConfig := new(integrationtypes.AWSServiceConfig)
|
||||
err = integrationtypes.UnmarshalJSON(config, serviceConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
svcConfigs[svcType] = serviceConfig
|
||||
}
|
||||
}
|
||||
|
||||
summaries := make([]integrationtypes.AWSServiceSummary, 0)
|
||||
|
||||
definitions, err := a.ServiceDefinitions.ListServiceDefinitions(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, def := range definitions {
|
||||
summary := integrationtypes.AWSServiceSummary{
|
||||
DefinitionMetadata: def.DefinitionMetadata,
|
||||
Config: nil,
|
||||
}
|
||||
|
||||
summary.Config = svcConfigs[summary.Id]
|
||||
|
||||
summaries = append(summaries, summary)
|
||||
}
|
||||
|
||||
slices.SortFunc(summaries, func(a, b integrationtypes.AWSServiceSummary) int {
|
||||
if a.DefinitionMetadata.Title < b.DefinitionMetadata.Title {
|
||||
return -1
|
||||
}
|
||||
if a.DefinitionMetadata.Title > b.DefinitionMetadata.Title {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
return &integrationtypes.GettableAWSServices{
|
||||
Services: summaries,
|
||||
}, nil
|
||||
}
|
||||
36
ee/modules/cloudintegrations/implcloudintegrations/module.go
Normal file
36
ee/modules/cloudintegrations/implcloudintegrations/module.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package implcloudintegrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegrations"
|
||||
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
|
||||
)
|
||||
|
||||
type module struct {
|
||||
store integrationtypes.Store
|
||||
providers map[integrationtypes.CloudProviderType]cloudintegrations.CloudProvider
|
||||
}
|
||||
|
||||
func NewModule(store integrationtypes.Store, providers map[integrationtypes.CloudProviderType]cloudintegrations.CloudProvider) cloudintegrations.Module {
|
||||
return &module{store: store}
|
||||
}
|
||||
|
||||
func (m *module) ListServices(ctx context.Context, orgID string, cloudProvider string, cloudAccountId *string) (any, error) {
|
||||
|
||||
provider, err := m.getProvider(cloudProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider.ListServices(ctx, orgID, cloudAccountId)
|
||||
}
|
||||
|
||||
func (m *module) getProvider(cloudProvider integrationtypes.CloudProviderType) (cloudintegrations.CloudProvider, error) {
|
||||
provider, ok := m.providers[cloudProvider]
|
||||
if !ok {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid cloud provider: %s", cloudProvider)
|
||||
}
|
||||
return provider, nil
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -10,7 +11,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
@@ -27,12 +27,12 @@ type APIHandlerOptions struct {
|
||||
RulesManager *rules.Manager
|
||||
UsageManager *usage.Manager
|
||||
IntegrationsController *integrations.Controller
|
||||
CloudIntegrationsController *cloudintegrations.Controller
|
||||
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
|
||||
GatewayUrl string
|
||||
// Querier Influx Interval
|
||||
FluxInterval time.Duration
|
||||
GlobalConfig global.Config
|
||||
Logger *slog.Logger // this is present in Signoz.Instrumentation but adding for quick access
|
||||
}
|
||||
|
||||
type APIHandler struct {
|
||||
@@ -46,13 +46,13 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.
|
||||
Reader: opts.DataConnector,
|
||||
RuleManager: opts.RulesManager,
|
||||
IntegrationsController: opts.IntegrationsController,
|
||||
CloudIntegrationsController: opts.CloudIntegrationsController,
|
||||
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
||||
FluxInterval: opts.FluxInterval,
|
||||
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
|
||||
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
|
||||
Signoz: signoz,
|
||||
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
|
||||
Logger: opts.Logger,
|
||||
}, config)
|
||||
|
||||
if err != nil {
|
||||
@@ -101,14 +101,12 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
}
|
||||
|
||||
func (ah *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
|
||||
ah.APIHandler.RegisterCloudIntegrationsRoutes(router, am)
|
||||
|
||||
router.HandleFunc(
|
||||
"/api/v1/cloud-integrations/{cloudProvider}/accounts/generate-connection-params",
|
||||
am.EditAccess(ah.CloudIntegrationsGenerateConnectionParams),
|
||||
).Methods(http.MethodGet)
|
||||
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -13,20 +14,14 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type CloudIntegrationConnectionParamsResponse struct {
|
||||
IngestionUrl string `json:"ingestion_url,omitempty"`
|
||||
IngestionKey string `json:"ingestion_key,omitempty"`
|
||||
SigNozAPIUrl string `json:"signoz_api_url,omitempty"`
|
||||
SigNozAPIKey string `json:"signoz_api_key,omitempty"`
|
||||
}
|
||||
// TODO: move this file with other cloud integration related code
|
||||
|
||||
func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseWriter, r *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
@@ -41,23 +36,21 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
|
||||
return
|
||||
}
|
||||
|
||||
cloudProvider := mux.Vars(r)["cloudProvider"]
|
||||
if cloudProvider != "aws" {
|
||||
RespondError(w, basemodel.BadRequest(fmt.Errorf(
|
||||
"cloud provider not supported: %s", cloudProvider,
|
||||
)), nil)
|
||||
cloudProviderString := mux.Vars(r)["cloudProvider"]
|
||||
|
||||
cloudProvider, err := integrationtypes.NewCloudProvider(cloudProviderString)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
apiKey, apiErr := ah.getOrCreateCloudIntegrationPAT(r.Context(), claims.OrgID, cloudProvider)
|
||||
if apiErr != nil {
|
||||
RespondError(w, basemodel.WrapApiError(
|
||||
apiErr, "couldn't provision PAT for cloud integration:",
|
||||
), nil)
|
||||
apiKey, err := ah.getOrCreateCloudIntegrationPAT(r.Context(), claims.OrgID, cloudProvider)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
result := CloudIntegrationConnectionParamsResponse{
|
||||
result := integrationtypes.GettableCloudIntegrationConnectionParams{
|
||||
SigNozAPIKey: apiKey,
|
||||
}
|
||||
|
||||
@@ -71,16 +64,17 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
|
||||
// Return the API Key (PAT) even if the rest of the params can not be deduced.
|
||||
// Params not returned from here will be requested from the user via form inputs.
|
||||
// This enables gracefully degraded but working experience even for non-cloud deployments.
|
||||
zap.L().Info("ingestion params and signoz api url can not be deduced since no license was found")
|
||||
ah.Respond(w, result)
|
||||
ah.opts.Logger.InfoContext(
|
||||
r.Context(),
|
||||
"ingestion params and signoz api url can not be deduced since no license was found",
|
||||
)
|
||||
render.Success(w, http.StatusOK, result)
|
||||
return
|
||||
}
|
||||
|
||||
signozApiUrl, apiErr := ah.getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key)
|
||||
if apiErr != nil {
|
||||
RespondError(w, basemodel.WrapApiError(
|
||||
apiErr, "couldn't deduce ingestion url and signoz api url",
|
||||
), nil)
|
||||
signozApiUrl, err := ah.getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,48 +83,41 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
|
||||
|
||||
gatewayUrl := ah.opts.GatewayUrl
|
||||
if len(gatewayUrl) > 0 {
|
||||
|
||||
ingestionKey, apiErr := getOrCreateCloudProviderIngestionKey(
|
||||
ingestionKeyString, err := ah.getOrCreateCloudProviderIngestionKey(
|
||||
r.Context(), gatewayUrl, license.Key, cloudProvider,
|
||||
)
|
||||
if apiErr != nil {
|
||||
RespondError(w, basemodel.WrapApiError(
|
||||
apiErr, "couldn't get or create ingestion key",
|
||||
), nil)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
result.IngestionKey = ingestionKey
|
||||
|
||||
result.IngestionKey = ingestionKeyString
|
||||
} else {
|
||||
zap.L().Info("ingestion key can't be deduced since no gateway url has been configured")
|
||||
ah.opts.Logger.InfoContext(
|
||||
r.Context(),
|
||||
"ingestion key can't be deduced since no gateway url has been configured",
|
||||
)
|
||||
}
|
||||
|
||||
ah.Respond(w, result)
|
||||
render.Success(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId string, cloudProvider string) (
|
||||
string, *basemodel.ApiError,
|
||||
) {
|
||||
func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId string, cloudProvider valuer.String) (string, error) {
|
||||
integrationPATName := fmt.Sprintf("%s integration", cloudProvider)
|
||||
|
||||
integrationUser, apiErr := ah.getOrCreateCloudIntegrationUser(ctx, orgId, cloudProvider)
|
||||
if apiErr != nil {
|
||||
return "", apiErr
|
||||
integrationUser, err := ah.getOrCreateCloudIntegrationUser(ctx, orgId, cloudProvider)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
orgIdUUID, err := valuer.NewUUID(orgId)
|
||||
if err != nil {
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't parse orgId: %w", err,
|
||||
))
|
||||
return "", err
|
||||
}
|
||||
|
||||
allPats, err := ah.Signoz.Modules.User.ListAPIKeys(ctx, orgIdUUID)
|
||||
if err != nil {
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't list PATs: %w", err,
|
||||
))
|
||||
return "", err
|
||||
}
|
||||
for _, p := range allPats {
|
||||
if p.UserID == integrationUser.ID && p.Name == integrationPATName {
|
||||
@@ -138,9 +125,10 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
|
||||
}
|
||||
}
|
||||
|
||||
zap.L().Info(
|
||||
ah.opts.Logger.InfoContext(
|
||||
ctx,
|
||||
"no PAT found for cloud integration, creating a new one",
|
||||
zap.String("cloudProvider", cloudProvider),
|
||||
slog.String("cloudProvider", cloudProvider.String()),
|
||||
)
|
||||
|
||||
newPAT, err := types.NewStorableAPIKey(
|
||||
@@ -150,68 +138,48 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't create cloud integration PAT: %w", err,
|
||||
))
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = ah.Signoz.Modules.User.CreateAPIKey(ctx, newPAT)
|
||||
if err != nil {
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't create cloud integration PAT: %w", err,
|
||||
))
|
||||
return "", err
|
||||
}
|
||||
return newPAT.Token, nil
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||
ctx context.Context, orgId string, cloudProvider string,
|
||||
) (*types.User, *basemodel.ApiError) {
|
||||
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider)
|
||||
// TODO: move this function out of handler and use proper module structure
|
||||
func (ah *APIHandler) getOrCreateCloudIntegrationUser(ctx context.Context, orgId string, cloudProvider valuer.String) (*types.User, error) {
|
||||
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider.String())
|
||||
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName))
|
||||
|
||||
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId))
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
password := types.MustGenerateFactorPassword(cloudIntegrationUser.ID.StringValue())
|
||||
|
||||
cloudIntegrationUser, err = ah.Signoz.Modules.User.GetOrCreateUser(ctx, cloudIntegrationUser, user.WithFactorPassword(password))
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't look for integration user: %w", err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cloudIntegrationUser, nil
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
|
||||
string, *basemodel.ApiError,
|
||||
) {
|
||||
// TODO: remove this struct from here
|
||||
type deploymentResponse struct {
|
||||
Name string `json:"name"`
|
||||
ClusterInfo struct {
|
||||
Region struct {
|
||||
DNS string `json:"dns"`
|
||||
} `json:"region"`
|
||||
} `json:"cluster"`
|
||||
}
|
||||
|
||||
// TODO: move this function out of handler and use proper module structure
|
||||
func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (string, error) {
|
||||
respBytes, err := ah.Signoz.Zeus.GetDeployment(ctx, licenseKey)
|
||||
if err != nil {
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't query for deployment info: error: %w", err,
|
||||
))
|
||||
return "", errors.WrapInternalf(err, errors.CodeInternal, "couldn't query for deployment info: error")
|
||||
}
|
||||
|
||||
resp := new(deploymentResponse)
|
||||
resp := new(integrationtypes.GettableDeployment)
|
||||
|
||||
err = json.Unmarshal(respBytes, resp)
|
||||
if err != nil {
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't unmarshal deployment info response: error: %w", err,
|
||||
))
|
||||
return "", errors.WrapInternalf(err, errors.CodeInternal, "couldn't unmarshal deployment info response")
|
||||
}
|
||||
|
||||
regionDns := resp.ClusterInfo.Region.DNS
|
||||
@@ -219,9 +187,10 @@ func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licens
|
||||
|
||||
if len(regionDns) < 1 || len(deploymentName) < 1 {
|
||||
// Fail early if actual response structure and expectation here ever diverge
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
return "", errors.NewInternalf(
|
||||
errors.CodeInternal,
|
||||
"deployment info response not in expected shape. couldn't determine region dns and deployment name",
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
signozApiUrl := fmt.Sprintf("https://%s.%s", deploymentName, regionDns)
|
||||
@@ -229,102 +198,85 @@ func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licens
|
||||
return signozApiUrl, nil
|
||||
}
|
||||
|
||||
type ingestionKey struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
// other attributes from gateway response not included here since they are not being used.
|
||||
}
|
||||
|
||||
type ingestionKeysSearchResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data []ingestionKey `json:"data"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type createIngestionKeyResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data ingestionKey `json:"data"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func getOrCreateCloudProviderIngestionKey(
|
||||
ctx context.Context, gatewayUrl string, licenseKey string, cloudProvider string,
|
||||
) (string, *basemodel.ApiError) {
|
||||
func (ah *APIHandler) getOrCreateCloudProviderIngestionKey(
|
||||
ctx context.Context, gatewayUrl string, licenseKey string, cloudProvider valuer.String,
|
||||
) (string, error) {
|
||||
cloudProviderKeyName := fmt.Sprintf("%s-integration", cloudProvider)
|
||||
|
||||
// see if the key already exists
|
||||
searchResult, apiErr := requestGateway[ingestionKeysSearchResponse](
|
||||
searchResult, err := requestGateway[integrationtypes.GettableIngestionKeysSearch](
|
||||
ctx,
|
||||
gatewayUrl,
|
||||
licenseKey,
|
||||
fmt.Sprintf("/v1/workspaces/me/keys/search?name=%s", cloudProviderKeyName),
|
||||
nil,
|
||||
ah.opts.Logger,
|
||||
)
|
||||
|
||||
if apiErr != nil {
|
||||
return "", basemodel.WrapApiError(
|
||||
apiErr, "couldn't search for cloudprovider ingestion key",
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if searchResult.Status != "success" {
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't search for cloudprovider ingestion key: status: %s, error: %s",
|
||||
return "", errors.NewInternalf(
|
||||
errors.CodeInternal,
|
||||
"couldn't search for cloud provider ingestion key: status: %s, error: %s",
|
||||
searchResult.Status, searchResult.Error,
|
||||
))
|
||||
}
|
||||
|
||||
for _, k := range searchResult.Data {
|
||||
if k.Name == cloudProviderKeyName {
|
||||
if len(k.Value) < 1 {
|
||||
// Fail early if actual response structure and expectation here ever diverge
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"ingestion keys search response not as expected",
|
||||
))
|
||||
}
|
||||
|
||||
return k.Value, nil
|
||||
}
|
||||
}
|
||||
|
||||
zap.L().Info(
|
||||
"no existing ingestion key found for cloud integration, creating a new one",
|
||||
zap.String("cloudProvider", cloudProvider),
|
||||
)
|
||||
createKeyResult, apiErr := requestGateway[createIngestionKeyResponse](
|
||||
ctx, gatewayUrl, licenseKey, "/v1/workspaces/me/keys",
|
||||
map[string]any{
|
||||
"name": cloudProviderKeyName,
|
||||
"tags": []string{"integration", cloudProvider},
|
||||
},
|
||||
)
|
||||
if apiErr != nil {
|
||||
return "", basemodel.WrapApiError(
|
||||
apiErr, "couldn't create cloudprovider ingestion key",
|
||||
)
|
||||
}
|
||||
|
||||
for _, k := range searchResult.Data {
|
||||
if k.Name != cloudProviderKeyName {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(k.Value) < 1 {
|
||||
// Fail early if actual response structure and expectation here ever diverge
|
||||
return "", errors.NewInternalf(errors.CodeInternal, "ingestion keys search response not as expected")
|
||||
}
|
||||
|
||||
return k.Value, nil
|
||||
}
|
||||
|
||||
ah.opts.Logger.InfoContext(
|
||||
ctx,
|
||||
"no existing ingestion key found for cloud integration, creating a new one",
|
||||
slog.String("cloudProvider", cloudProvider.String()),
|
||||
)
|
||||
|
||||
createKeyResult, err := requestGateway[integrationtypes.GettableCreateIngestionKey](
|
||||
ctx, gatewayUrl, licenseKey, "/v1/workspaces/me/keys",
|
||||
map[string]any{
|
||||
"name": cloudProviderKeyName,
|
||||
"tags": []string{"integration", cloudProvider.String()},
|
||||
},
|
||||
ah.opts.Logger,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if createKeyResult.Status != "success" {
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't create cloudprovider ingestion key: status: %s, error: %s",
|
||||
return "", errors.NewInternalf(
|
||||
errors.CodeInternal,
|
||||
"couldn't create cloud provider ingestion key: status: %s, error: %s",
|
||||
createKeyResult.Status, createKeyResult.Error,
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
ingestionKey := createKeyResult.Data.Value
|
||||
if len(ingestionKey) < 1 {
|
||||
ingestionKeyString := createKeyResult.Data.Value
|
||||
if len(ingestionKeyString) < 1 {
|
||||
// Fail early if actual response structure and expectation here ever diverge
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
return "", errors.NewInternalf(errors.CodeInternal,
|
||||
"ingestion key creation response not as expected",
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
return ingestionKey, nil
|
||||
return ingestionKeyString, nil
|
||||
}
|
||||
|
||||
func requestGateway[ResponseType any](
|
||||
ctx context.Context, gatewayUrl string, licenseKey string, path string, payload any,
|
||||
) (*ResponseType, *basemodel.ApiError) {
|
||||
ctx context.Context, gatewayUrl, licenseKey, path string, payload any, logger *slog.Logger,
|
||||
) (*ResponseType, error) {
|
||||
|
||||
baseUrl := strings.TrimSuffix(gatewayUrl, "/")
|
||||
reqUrl := fmt.Sprintf("%s%s", baseUrl, path)
|
||||
@@ -335,13 +287,12 @@ func requestGateway[ResponseType any](
|
||||
"X-Consumer-Groups": "ns:default",
|
||||
}
|
||||
|
||||
return requestAndParseResponse[ResponseType](ctx, reqUrl, headers, payload)
|
||||
return requestAndParseResponse[ResponseType](ctx, reqUrl, headers, payload, logger)
|
||||
}
|
||||
|
||||
func requestAndParseResponse[ResponseType any](
|
||||
ctx context.Context, url string, headers map[string]string, payload any,
|
||||
) (*ResponseType, *basemodel.ApiError) {
|
||||
|
||||
ctx context.Context, url string, headers map[string]string, payload any, logger *slog.Logger,
|
||||
) (*ResponseType, error) {
|
||||
reqMethod := http.MethodGet
|
||||
var reqBody io.Reader
|
||||
if payload != nil {
|
||||
@@ -349,18 +300,14 @@ func requestAndParseResponse[ResponseType any](
|
||||
|
||||
bodyJson, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't serialize request payload to JSON: %w", err,
|
||||
))
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't marshal payload")
|
||||
}
|
||||
reqBody = bytes.NewBuffer([]byte(bodyJson))
|
||||
reqBody = bytes.NewBuffer(bodyJson)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, reqMethod, url, reqBody)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't prepare request: %w", err,
|
||||
))
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't create req")
|
||||
}
|
||||
|
||||
for k, v := range headers {
|
||||
@@ -373,23 +320,26 @@ func requestAndParseResponse[ResponseType any](
|
||||
|
||||
response, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't make request: %w", err))
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't make req")
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
defer func() {
|
||||
err = response.Body.Close()
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "couldn't close response body", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
respBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't read response: %w", err))
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't read response body")
|
||||
}
|
||||
|
||||
var resp ResponseType
|
||||
|
||||
err = json.Unmarshal(respBody, &resp)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't unmarshal gateway response into %T", resp,
|
||||
))
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't unmarshal response body")
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
|
||||
@@ -37,7 +37,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/opamp"
|
||||
@@ -121,13 +120,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
)
|
||||
}
|
||||
|
||||
cloudIntegrationsController, err := cloudintegrations.NewController(signoz.SQLStore)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"couldn't create cloud provider integrations controller: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
// ingestion pipelines manager
|
||||
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
|
||||
signoz.SQLStore,
|
||||
@@ -161,11 +153,11 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
RulesManager: rm,
|
||||
UsageManager: usageManager,
|
||||
IntegrationsController: integrationsController,
|
||||
CloudIntegrationsController: cloudIntegrationsController,
|
||||
LogsParsingPipelineController: logParsingPipelineController,
|
||||
FluxInterval: config.Querier.FluxInterval,
|
||||
GatewayUrl: config.Gateway.URL.String(),
|
||||
GlobalConfig: config.Global,
|
||||
Logger: signoz.Instrumentation.Logger(),
|
||||
}
|
||||
|
||||
apiHandler, err := api.NewAPIHandler(apiOpts, signoz, config)
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/times"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/units"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/formatter"
|
||||
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
|
||||
@@ -335,7 +335,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
|
||||
|
||||
prevState := r.State()
|
||||
|
||||
valueFormatter := units.FormatterFromUnit(r.Unit())
|
||||
valueFormatter := formatter.FromUnit(r.Unit())
|
||||
|
||||
var res ruletypes.Vector
|
||||
var err error
|
||||
|
||||
@@ -78,7 +78,7 @@ module.exports = {
|
||||
// TODO: Change to 'error' after fixing ~80 empty function placeholders in providers/contexts
|
||||
'@typescript-eslint/no-empty-function': 'off', // Disallows empty function bodies
|
||||
'@typescript-eslint/no-var-requires': 'error', // Disallows require() in TypeScript (use import instead)
|
||||
'@typescript-eslint/ban-ts-comment': 'warn', // Allows @ts-ignore comments (sometimes needed for third-party libs)
|
||||
'@typescript-eslint/ban-ts-comment': 'off', // Allows @ts-ignore comments (sometimes needed for third-party libs)
|
||||
'no-empty-function': 'off', // Disabled in favor of TypeScript version above
|
||||
|
||||
// React rules
|
||||
@@ -146,49 +146,6 @@ module.exports = {
|
||||
|
||||
// SonarJS - code quality and complexity
|
||||
'sonarjs/no-duplicate-string': 'off', // Disabled - can be noisy (enable periodically to check)
|
||||
|
||||
// State management governance
|
||||
// Approved patterns: Zustand, nuqs (URL state), react-query (server state), useState/useRef/useReducer, localStorage/sessionStorage for simple cases
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'redux',
|
||||
message:
|
||||
'[State mgmt] redux is deprecated. Migrate to Zustand, nuqs, or react-query.',
|
||||
},
|
||||
{
|
||||
name: 'react-redux',
|
||||
message:
|
||||
'[State mgmt] react-redux is deprecated. Migrate to Zustand, nuqs, or react-query.',
|
||||
},
|
||||
{
|
||||
name: 'xstate',
|
||||
message:
|
||||
'[State mgmt] xstate is deprecated. Migrate to Zustand or react-query.',
|
||||
},
|
||||
{
|
||||
name: '@xstate/react',
|
||||
message:
|
||||
'[State mgmt] @xstate/react is deprecated. Migrate to Zustand or react-query.',
|
||||
},
|
||||
{
|
||||
// Restrict React Context — useState/useRef/useReducer remain allowed
|
||||
name: 'react',
|
||||
importNames: ['createContext', 'useContext'],
|
||||
message:
|
||||
'[State mgmt] React Context is deprecated. Migrate shared state to Zustand.',
|
||||
},
|
||||
{
|
||||
// immer used standalone as a store pattern is deprecated; Zustand bundles it internally
|
||||
name: 'immer',
|
||||
message:
|
||||
'[State mgmt] Direct immer usage is deprecated. Use Zustand (which integrates immer via the immer middleware) instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
||||
@@ -117,7 +117,6 @@
|
||||
"lucide-react": "0.498.0",
|
||||
"mini-css-extract-plugin": "2.4.5",
|
||||
"motion": "12.4.13",
|
||||
"nuqs": "2.8.8",
|
||||
"overlayscrollbars": "^2.8.1",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"papaparse": "5.4.1",
|
||||
@@ -131,7 +130,7 @@
|
||||
"react-dom": "18.2.0",
|
||||
"react-drag-listview": "2.0.0",
|
||||
"react-error-boundary": "4.0.11",
|
||||
"react-force-graph-2d": "^1.29.1",
|
||||
"react-force-graph": "^1.43.0",
|
||||
"react-full-screen": "1.1.1",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-helmet-async": "1.3.0",
|
||||
@@ -163,8 +162,7 @@
|
||||
"webpack": "5.94.0",
|
||||
"webpack-dev-server": "^5.2.1",
|
||||
"webpack-retry-chunk-load-plugin": "3.1.1",
|
||||
"xstate": "^4.31.0",
|
||||
"zustand": "5.0.11"
|
||||
"xstate": "^4.31.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -288,5 +286,6 @@
|
||||
"brace-expansion": "^2.0.2",
|
||||
"on-headers": "^1.1.0",
|
||||
"tmp": "0.2.4"
|
||||
}
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,5 @@
|
||||
"pipelines": "Pipelines",
|
||||
"archives": "Archives",
|
||||
"logs_to_metrics": "Logs To Metrics",
|
||||
"roles": "Roles",
|
||||
"members": "Members"
|
||||
"roles": "Roles"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,5 @@
|
||||
"pipelines": "Pipelines",
|
||||
"archives": "Archives",
|
||||
"logs_to_metrics": "Logs To Metrics",
|
||||
"roles": "Roles",
|
||||
"members": "Members"
|
||||
"roles": "Roles"
|
||||
}
|
||||
|
||||
@@ -297,6 +297,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
}, [isLoggedInState, pathname, user, isOldRoute, currentRoute, location]);
|
||||
|
||||
// NOTE: disabling this rule as there is no need to have div
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
|
||||
@@ -218,8 +218,12 @@ function App(): JSX.Element {
|
||||
pathname === ROUTES.ONBOARDING ||
|
||||
pathname.startsWith('/public/dashboard/')
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.Pylon('hideChatBubble');
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.Pylon('showChatBubble');
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
@@ -44,6 +44,7 @@ const dashboardVariablesQuery = async (
|
||||
} catch (error) {
|
||||
const formattedError = ErrorResponseHandler(error as AxiosError);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||
throw { message: 'Error fetching data', details: formattedError };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import axios from 'api';
|
||||
|
||||
import { getFieldKeys } from '../getFieldKeys';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import axios from 'api';
|
||||
|
||||
import { getFieldValues } from '../getFieldValues';
|
||||
|
||||
@@ -1006,18 +1006,6 @@ export interface MetricsexplorertypesUpdateMetricMetadataRequestDTO {
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface MetrictypesComparisonSpaceAggregationParamDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
operator: string;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
export enum MetrictypesSpaceAggregationDTO {
|
||||
sum = 'sum',
|
||||
avg = 'avg',
|
||||
@@ -1379,7 +1367,6 @@ export interface Querybuildertypesv5LogAggregationDTO {
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5MetricAggregationDTO {
|
||||
comparisonSpaceAggregationParam?: MetrictypesComparisonSpaceAggregationParamDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { QueryClient } from 'react-query';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import post from 'api/v2/sessions/rotate/post';
|
||||
|
||||
@@ -50,7 +50,6 @@ export interface HostListResponse {
|
||||
total: number;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingK8SAgentMetrics: boolean;
|
||||
endTimeBeforeRetention: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
26
frontend/src/api/metricsExplorer/v2/getMetricMetadata.ts
Normal file
26
frontend/src/api/metricsExplorer/v2/getMetricMetadata.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ApiV2Instance as axios } from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { MetricMetadataResponse } from 'types/api/metricsExplorer/v2/getMetricMetadata';
|
||||
|
||||
export const getMetricMetadata = async (
|
||||
metricName: string,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<SuccessResponseV2<MetricMetadataResponse> | ErrorResponseV2> => {
|
||||
try {
|
||||
const encodedMetricName = encodeURIComponent(metricName);
|
||||
const response = await axios.get(`/metrics/${encodedMetricName}/metadata`, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import {
|
||||
MetricRangePayloadV5,
|
||||
|
||||
@@ -274,6 +274,7 @@ function convertDistributionData(
|
||||
distributionData: DistributionData,
|
||||
legendMap: Record<string, string>,
|
||||
): any {
|
||||
// eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
// Convert V5 distribution format to legacy histogram format
|
||||
return {
|
||||
...distributionData,
|
||||
@@ -414,6 +415,7 @@ export function convertV5ResponseToLegacy(
|
||||
if (legacyResponse.payload?.data?.result) {
|
||||
legacyResponse.payload.data.result = legacyResponse.payload.data.result.map(
|
||||
(queryData: any) => {
|
||||
// eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
const newQueryData = cloneDeep(queryData);
|
||||
newQueryData.legend = legendMap[queryData.queryName];
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string, simple-import-sort/imports, @typescript-eslint/indent, no-mixed-spaces-and-tabs */
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderFormula,
|
||||
IBuilderQuery,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import {
|
||||
ClickHouseQuery,
|
||||
LogAggregation,
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { prepareQueryRangePayloadV5 } from './prepareQueryRangePayloadV5';
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ export function createAggregation(
|
||||
* Converts query builder data to V5 builder queries
|
||||
*/
|
||||
export function convertBuilderQueriesToV5(
|
||||
builderQueries: Record<string, any>,
|
||||
builderQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
requestType: RequestType,
|
||||
panelType?: PANEL_TYPES,
|
||||
): QueryEnvelope[] {
|
||||
@@ -467,7 +467,7 @@ export function convertTraceOperatorToV5(
|
||||
* Converts PromQL queries to V5 format
|
||||
*/
|
||||
export function convertPromQueriesToV5(
|
||||
promQueries: Record<string, any>,
|
||||
promQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
): QueryEnvelope[] {
|
||||
return Object.entries(promQueries).map(
|
||||
([queryName, queryData]): QueryEnvelope => ({
|
||||
@@ -488,7 +488,7 @@ export function convertPromQueriesToV5(
|
||||
* Converts ClickHouse queries to V5 format
|
||||
*/
|
||||
export function convertClickHouseQueriesToV5(
|
||||
chQueries: Record<string, any>,
|
||||
chQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
): QueryEnvelope[] {
|
||||
return Object.entries(chQueries).map(
|
||||
([queryName, queryData]): QueryEnvelope => ({
|
||||
@@ -508,8 +508,9 @@ export function convertClickHouseQueriesToV5(
|
||||
* Helper function to reduce query arrays to objects
|
||||
*/
|
||||
function reduceQueriesToObject(
|
||||
queryArray: any[],
|
||||
queryArray: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
): { queries: Record<string, any>; legends: Record<string, string> } {
|
||||
// eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
const legends: Record<string, string> = {};
|
||||
const queries = queryArray.reduce((acc, queryItem) => {
|
||||
if (!queryItem.query) {
|
||||
@@ -518,7 +519,7 @@ function reduceQueriesToObject(
|
||||
acc[queryItem.name] = queryItem;
|
||||
legends[queryItem.name] = queryItem.legend;
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
}, {} as Record<string, any>); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
return { queries, legends };
|
||||
}
|
||||
@@ -588,6 +589,7 @@ export const prepareQueryRangePayloadV5 = ({
|
||||
limit: formulaData.limit ?? undefined,
|
||||
legend: isEmpty(formulaData.legend) ? undefined : formulaData.legend,
|
||||
order: formulaData.orderBy?.map(
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
(order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
|
||||
@@ -10,6 +10,7 @@ function ErrorIcon({ ...props }: ErrorIconProps): JSX.Element {
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
|
||||
@@ -96,6 +96,7 @@ export function FilterSelect({
|
||||
key={filterType.toString()}
|
||||
placeholder={placeholder}
|
||||
showSearch
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...(isMultiple ? { mode: 'multiple' } : {})}
|
||||
options={mergedOptions}
|
||||
loading={isFetching}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { LoadingOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
@@ -91,6 +90,7 @@ const getColumnSearchProps = (
|
||||
clearFilters,
|
||||
close,
|
||||
}): JSX.Element => (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div style={{ padding: 8 }} onKeyDown={(e): void => e.stopPropagation()}>
|
||||
<Input
|
||||
ref={searchInput}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useQuery } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Card, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||
import { getWidgetQuery } from 'pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Col, Row } from 'antd';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||
import { Col, Row } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import {
|
||||
ChangeEvent,
|
||||
Dispatch,
|
||||
|
||||
@@ -92,6 +92,7 @@ const getDateRange = (
|
||||
return { from, to };
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function CustomTimePickerPopoverContent({
|
||||
isLiveLogsEnabled,
|
||||
minTime,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { DatePicker } from 'antd';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
@@ -45,6 +45,7 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
|
||||
|
||||
// Using any type here because antd's DatePicker expects its own internal Dayjs type
|
||||
// which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc).
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
||||
const disabledDate = (current: any): boolean => {
|
||||
const currentDay = dayjs(current);
|
||||
return currentDay.isAfter(dayjs());
|
||||
|
||||
@@ -124,6 +124,7 @@ const filterAndSortTimezones = (
|
||||
export const generateTimezoneData = (
|
||||
includeEtcTimezones = false,
|
||||
): Timezone[] => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const allTimezones = (Intl as any).supportedValuesOf('timeZone');
|
||||
const timezones: Timezone[] = [];
|
||||
|
||||
|
||||
@@ -37,7 +37,13 @@ function DraggableTableRow({
|
||||
drop(drag(ref));
|
||||
|
||||
return (
|
||||
<tr ref={ref} className={className} style={{ ...style }} {...restProps} />
|
||||
<tr
|
||||
ref={ref}
|
||||
className={className}
|
||||
style={{ ...style }}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ function withErrorBoundary<P extends Record<string, unknown>>(
|
||||
}}
|
||||
onError={onError}
|
||||
>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<WrappedComponent {...props} />
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
|
||||
@@ -19,8 +19,11 @@ jest.mock('react-query', () => ({
|
||||
const mockError: APIError = new APIError({
|
||||
httpStatusCode: 400,
|
||||
error: {
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
message: 'Something went wrong while processing your request.',
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
code: 'An error occurred',
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
url: 'https://example.com/docs',
|
||||
errors: [
|
||||
{ message: 'First error detail' },
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { ReactNode } from 'react';
|
||||
import { Popover, PopoverProps } from 'antd';
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import ExplorerCard from '../ExplorerCard';
|
||||
|
||||
const historyReplace = jest.fn();
|
||||
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
|
||||
@@ -37,6 +37,7 @@ export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const omitIdFromQuery = (query: Query | null): any => ({
|
||||
...query,
|
||||
builder: {
|
||||
|
||||
@@ -310,6 +310,7 @@ export const createDragSelectPlugin = (): Plugin<
|
||||
const top = chart.chartArea.top - 5;
|
||||
const bottom = chart.chartArea.bottom + 5;
|
||||
|
||||
/* eslint-disable-next-line no-param-reassign */
|
||||
chart.ctx.fillStyle = pluginOptions.color;
|
||||
chart.ctx.fillRect(left, top, right - left, bottom - top);
|
||||
}
|
||||
|
||||
@@ -142,6 +142,7 @@ export const createIntersectionCursorPlugin = (): Plugin<
|
||||
const { top, bottom, left, right } = chart.chartArea;
|
||||
|
||||
chart.ctx.beginPath();
|
||||
/* eslint-disable-next-line no-param-reassign */
|
||||
chart.ctx.strokeStyle = pluginOptions.color;
|
||||
chart.ctx.setLineDash(lineDashData);
|
||||
chart.ctx.moveTo(left, positionY);
|
||||
@@ -150,6 +151,7 @@ export const createIntersectionCursorPlugin = (): Plugin<
|
||||
|
||||
chart.ctx.beginPath();
|
||||
chart.ctx.setLineDash(lineDashData);
|
||||
/* eslint-disable-next-line no-param-reassign */
|
||||
chart.ctx.strokeStyle = pluginOptions.color;
|
||||
chart.ctx.moveTo(positionX, top);
|
||||
chart.ctx.lineTo(positionX, bottom);
|
||||
|
||||
@@ -55,6 +55,7 @@ export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => ({
|
||||
)
|
||||
: null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
items?.forEach((item: Record<any, any>, index: number) => {
|
||||
const li = document.createElement('li');
|
||||
li.style.alignItems = 'center';
|
||||
@@ -64,6 +65,7 @@ export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => ({
|
||||
// li.style.marginTop = '5px';
|
||||
|
||||
li.onclick = (): void => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const { type } = chart.config;
|
||||
if (type === 'pie' || type === 'doughnut') {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { PrecisionOptionsEnum } from '../types';
|
||||
import { getYAxisFormattedValue } from '../yAxisConfig';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { ChartData } from 'chart.js';
|
||||
|
||||
export const hasData = (data: ChartData): boolean => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MutableRefObject } from 'react';
|
||||
import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js';
|
||||
// eslint-disable-next-line import/namespace -- side-effect import that registers Chart.js date adapter
|
||||
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
@@ -207,6 +208,7 @@ export const getGraphOptions = (
|
||||
cubicInterpolationMode: 'monotone',
|
||||
},
|
||||
point: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
hoverBackgroundColor: (ctx: any): string => {
|
||||
if (ctx?.element?.options?.borderColor) {
|
||||
return ctx.element.options.borderColor;
|
||||
@@ -233,6 +235,7 @@ export const getGraphOptions = (
|
||||
);
|
||||
|
||||
if (interactions[0]) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
nearestDatasetIndex.current = interactions[0].datasetIndex;
|
||||
}
|
||||
}
|
||||
@@ -245,11 +248,6 @@ declare module 'chart.js' {
|
||||
}
|
||||
}
|
||||
|
||||
const intlNumberFormatter = new Intl.NumberFormat('en-US', {
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 20,
|
||||
});
|
||||
|
||||
/**
|
||||
* Formats a number for display, preserving leading zeros after the decimal point
|
||||
* and showing up to DEFAULT_SIGNIFICANT_DIGITS digits after the first non-zero decimal digit.
|
||||
@@ -272,7 +270,10 @@ export const formatDecimalWithLeadingZeros = (
|
||||
}
|
||||
|
||||
// Use toLocaleString to get a full decimal representation without scientific notation.
|
||||
const numStr = intlNumberFormatter.format(value);
|
||||
const numStr = value.toLocaleString('en-US', {
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 20,
|
||||
});
|
||||
|
||||
const [integerPart, decimalPart = ''] = numStr.split('.');
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Chart, TimeUnit } from 'chart.js';
|
||||
import { AppState } from 'store/reducers';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
// Mock dependencies before imports
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { toast } from '@signozhq/sonner';
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
// Mock dependencies before imports
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Mock dependencies before imports
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
@@ -144,6 +143,7 @@ function HostMetricsDetails({
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [host]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -207,6 +207,7 @@ function HostMetricsDetails({
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -489,6 +490,7 @@ function HostMetricsDetails({
|
||||
>
|
||||
<Radio.Button
|
||||
className={
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
selectedView === VIEW_TYPES.METRICS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.METRICS}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
@@ -121,6 +122,7 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
|
||||
|
||||
const renderFooter = useCallback(
|
||||
(): JSX.Element | null => (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>
|
||||
{isFetching ? (
|
||||
<div className="logs-loading-skeleton"> Loading more logs ... </div>
|
||||
|
||||
@@ -34,6 +34,7 @@ function InputComponent({
|
||||
addonBefore={addonBefore}
|
||||
onBlur={onBlurHandler}
|
||||
onPressEnter={onPressEnterHandler}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -112,6 +112,8 @@ function LaunchChatSupport({
|
||||
} else {
|
||||
logEvent(eventName, attributes);
|
||||
if (window.pylon && !chatMessageDisabled) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.Pylon('showNewMessage', defaultTo(message, ''));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ function QueryBuilderSearchWrapper({
|
||||
setContextQuery({ ...nextQuery });
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
if (!contextQuery || !isEdit) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useCopyToClipboard, useLocation } from 'react-use';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
@@ -55,7 +55,6 @@ import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
|
||||
|
||||
import './LogDetails.styles.scss';
|
||||
|
||||
/* eslint-disable-next-line sonarjs/cognitive-complexity */
|
||||
function LogDetailInner({
|
||||
log,
|
||||
onClose,
|
||||
@@ -110,7 +109,6 @@ function LogDetailInner({
|
||||
|
||||
// Keyboard navigation - handle up/down arrow keys
|
||||
// Only listen when in OVERVIEW tab
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
useEffect(() => {
|
||||
if (
|
||||
!logs ||
|
||||
@@ -426,6 +424,7 @@ function LogDetailInner({
|
||||
>
|
||||
<Radio.Button
|
||||
className={
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
selectedView === VIEW_TYPES.OVERVIEW ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.OVERVIEW}
|
||||
@@ -574,9 +573,11 @@ function LogDetailInner({
|
||||
function LogDetail(props: LogDetailProps): JSX.Element {
|
||||
const { log } = props;
|
||||
if (!log) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <LogDetailInner {...(props as LogDetailInnerProps)} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ function AddToQueryHOC({
|
||||
]);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div className={cx('addToQueryContainer', fontSize)} onClick={handleQueryAdd}>
|
||||
<Popover
|
||||
overlayClassName="drawer-popover"
|
||||
|
||||
@@ -70,6 +70,9 @@
|
||||
padding-left: 0;
|
||||
}
|
||||
transition: background-color 0.2s ease-in;
|
||||
&:hover {
|
||||
background-color: rgba(171, 189, 255, 0.04) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.log-selected-fields {
|
||||
@@ -180,6 +183,11 @@
|
||||
.log-value {
|
||||
color: var(--text-slate-400);
|
||||
}
|
||||
.log-line {
|
||||
&:hover {
|
||||
background-color: var(--text-vanilla-200) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Card } from 'antd';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import styled from 'styled-components';
|
||||
@@ -48,12 +49,6 @@ export const Container = styled(Card)<{
|
||||
|
||||
${({ $isActiveLog, $isDarkMode, $logType }): string =>
|
||||
getActiveLogBackground($isActiveLog, $isDarkMode, $logType)}
|
||||
}
|
||||
|
||||
&:hover .ant-card-body {
|
||||
${({ $isDarkMode, $logType }): string =>
|
||||
getActiveLogBackground(true, $isDarkMode, $logType)}
|
||||
}
|
||||
`;
|
||||
|
||||
export const LogContainer = styled.div<LogContainerProps>`
|
||||
|
||||
@@ -78,6 +78,7 @@ const SEVERITY_VARIANT_CLASSES: Record<string, string> = {
|
||||
Wrn: 'severity-warn-4',
|
||||
|
||||
// Error variants - cherry-600 to cherry-200
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
ERROR: 'severity-error-0',
|
||||
Error: 'severity-error-1',
|
||||
error: 'severity-error-2',
|
||||
@@ -89,9 +90,11 @@ const SEVERITY_VARIANT_CLASSES: Record<string, string> = {
|
||||
FAIL: 'severity-error-0',
|
||||
|
||||
// Fatal variants - sakura-600 to sakura-200
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
FATAL: 'severity-fatal-0',
|
||||
Fatal: 'severity-fatal-1',
|
||||
fatal: 'severity-fatal-2',
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
critical: 'severity-fatal-3',
|
||||
Critical: 'severity-fatal-4',
|
||||
CRITICAL: 'severity-fatal-0',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { getLogIndicatorType, getLogIndicatorTypeForTable } from './utils';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { blue } from '@ant-design/colors';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Col, Row, Space } from 'antd';
|
||||
@@ -7,6 +8,7 @@ import styled from 'styled-components';
|
||||
import {
|
||||
getActiveLogBackground,
|
||||
getCustomHighlightBackground,
|
||||
getDefaultLogBackground,
|
||||
} from 'utils/logs';
|
||||
|
||||
import { RawLogContentProps } from './types';
|
||||
@@ -46,9 +48,7 @@ export const RawLogViewContainer = styled(Row)<{
|
||||
${({ $isReadOnly, $isActiveLog, $isDarkMode, $logType }): string =>
|
||||
$isActiveLog
|
||||
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)
|
||||
: !$isReadOnly
|
||||
? `&:hover { ${getActiveLogBackground(true, $isDarkMode, $logType)} }`
|
||||
: ''}
|
||||
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
|
||||
|
||||
${({ $isHightlightedLog, $isDarkMode }): string =>
|
||||
$isHightlightedLog
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
// We do not need any title and data index for the log state indicator
|
||||
title: '',
|
||||
dataIndex: '',
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
key: 'state-indicator',
|
||||
accessorKey: 'state-indicator',
|
||||
id: 'state-indicator',
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Input, InputNumber, Popover, Tooltip, Typography } from 'antd';
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
@@ -77,6 +80,7 @@ function OptionsMenu({
|
||||
};
|
||||
|
||||
const handleSearchValueChange = useDebouncedFn((event): void => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const value = event?.target?.value || '';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
import React, { useState } from 'react';
|
||||
import { CheckOutlined, CopyOutlined } from '@ant-design/icons';
|
||||
import cx from 'classnames';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
@@ -51,6 +53,7 @@ function Code({
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
style={a11yDark}
|
||||
language={match[1]}
|
||||
@@ -113,6 +116,7 @@ function MarkdownRenderer({
|
||||
<ReactMarkdown
|
||||
rehypePlugins={[rehypeRaw as any]}
|
||||
components={{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
a: Link,
|
||||
pre: ({ children }) =>
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
// ─── Table wrapper ────────────────────────────────────────────────────────────
|
||||
.members-table-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
// ─── Ant Design Table overrides ───────────────────────────────────────────────
|
||||
.members-table {
|
||||
// Flatten Ant Design chrome
|
||||
.ant-table {
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ant-table-container {
|
||||
border-radius: 0 !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
// ── Header ──────────────────────────────────────────────────────────────
|
||||
.ant-table-thead {
|
||||
> tr > th,
|
||||
> tr > td {
|
||||
background: var(--bg-ink-500, #0b0c0e);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
color: var(--vanilla-400, #c0c1c3);
|
||||
line-height: 18px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: none !important;
|
||||
border-top: none !important;
|
||||
|
||||
// Remove the column divider pseudo-element
|
||||
&::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// Sorter icon color
|
||||
.ant-table-column-sorter {
|
||||
color: var(--vanilla-400, #c0c1c3);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.ant-table-column-sorter-up.active,
|
||||
.ant-table-column-sorter-down.active {
|
||||
color: var(--vanilla-100, #ffffff);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Body rows ────────────────────────────────────────────────────────────
|
||||
.ant-table-tbody {
|
||||
> tr > td {
|
||||
border-bottom: none !important;
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
// Even-indexed rows (0, 2, 4 …) get a subtle tint — matches Figma
|
||||
> tr.members-table-row--tinted > td {
|
||||
background: rgba(171, 189, 255, 0.02);
|
||||
}
|
||||
|
||||
// Hover — slightly brighter tint
|
||||
> tr:hover > td {
|
||||
background: rgba(171, 189, 255, 0.04) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove outer table border
|
||||
.ant-table-wrapper,
|
||||
.ant-table-container,
|
||||
.ant-spin-nested-loading,
|
||||
.ant-spin-container {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
// ── Status cell — right-align badge and add 4px right padding ─────────
|
||||
.member-status-cell {
|
||||
padding-right: 4px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Name / Email cell ────────────────────────────────────────────────────────
|
||||
.member-name-email-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 22px;
|
||||
overflow: hidden;
|
||||
|
||||
.member-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vanilla-400, #c0c1c3);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.member-email {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--slate-50, #62687c);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
flex: 1 0 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Permissions dash ─────────────────────────────────────────────────────────
|
||||
.member-dash {
|
||||
font-size: 14px;
|
||||
color: var(--slate-50, #62687c);
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
// ─── Joined On ────────────────────────────────────────────────────────────────
|
||||
.member-joined-date {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--vanilla-400, #c0c1c3);
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.member-joined-dash {
|
||||
font-size: 14px;
|
||||
color: var(--slate-50, #62687c);
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
// ─── Empty state ─────────────────────────────────────────────────────────────
|
||||
.members-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 16px;
|
||||
gap: 8px;
|
||||
|
||||
&__emoji {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--vanilla-400, #c0c1c3);
|
||||
margin: 0;
|
||||
line-height: 20px;
|
||||
|
||||
strong {
|
||||
font-weight: 500;
|
||||
color: var(--vanilla-100, #ffffff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Pagination ───────────────────────────────────────────────────────────────
|
||||
.members-table-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 8px 16px;
|
||||
|
||||
.ant-pagination-total-text {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.members-pagination-range {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 12px;
|
||||
color: var(--vanilla-400, #c0c1c3);
|
||||
}
|
||||
|
||||
.members-pagination-total {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 12px;
|
||||
color: var(--vanilla-400, #c0c1c3);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Light mode overrides ─────────────────────────────────────────────────────
|
||||
.lightMode {
|
||||
.members-table {
|
||||
.ant-table-thead {
|
||||
> tr > th,
|
||||
> tr > td {
|
||||
background: #f5f5f7;
|
||||
color: #5a5f6e;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-tbody {
|
||||
> tr.members-table-row--tinted > td {
|
||||
background: rgba(0, 0, 0, 0.015);
|
||||
}
|
||||
|
||||
> tr:hover > td {
|
||||
background: rgba(0, 0, 0, 0.03) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.member-name-email-cell {
|
||||
.member-name {
|
||||
color: #1d1f29;
|
||||
}
|
||||
|
||||
.member-email {
|
||||
color: #5a5f6e;
|
||||
}
|
||||
}
|
||||
|
||||
.member-dash,
|
||||
.member-joined-dash {
|
||||
color: #5a5f6e;
|
||||
}
|
||||
|
||||
.member-joined-date {
|
||||
color: #1d1f29;
|
||||
}
|
||||
|
||||
.members-empty-state {
|
||||
&__text {
|
||||
color: #5a5f6e;
|
||||
|
||||
strong {
|
||||
color: #1d1f29;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.members-pagination-range,
|
||||
.members-pagination-total {
|
||||
color: #5a5f6e;
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
import { Badge } from '@signozhq/badge';
|
||||
import { Pagination, Table, Tooltip } from 'antd';
|
||||
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
import './MembersTable.styles.scss';
|
||||
|
||||
export interface MemberRow {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: ROLES;
|
||||
status: 'Active' | 'Invited';
|
||||
joinedOn: string | null;
|
||||
}
|
||||
|
||||
interface MembersTableProps {
|
||||
data: MemberRow[];
|
||||
loading: boolean;
|
||||
total: number;
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
searchQuery: string;
|
||||
onPageChange: (page: number) => void;
|
||||
onSortChange?: (
|
||||
sorter: SorterResult<MemberRow> | SorterResult<MemberRow>[],
|
||||
) => void;
|
||||
}
|
||||
|
||||
function formatRoleLabel(role: string): string {
|
||||
return role.charAt(0).toUpperCase() + role.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
function NameEmailCell({
|
||||
name,
|
||||
email,
|
||||
}: {
|
||||
name: string;
|
||||
email: string;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="member-name-email-cell">
|
||||
{name && (
|
||||
<span className="member-name" title={name}>
|
||||
{name}
|
||||
</span>
|
||||
)}
|
||||
<Tooltip title={email} overlayClassName="member-tooltip">
|
||||
<span className="member-email">{email}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: MemberRow['status'] }): JSX.Element {
|
||||
if (status === 'Active') {
|
||||
return (
|
||||
<Badge color="forest" variant="outline">
|
||||
ACTIVE
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge color="amber" variant="outline">
|
||||
INVITED
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
function MembersEmptyState({
|
||||
searchQuery,
|
||||
}: {
|
||||
searchQuery: string;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="members-empty-state">
|
||||
<span className="members-empty-state__emoji">🧐</span>
|
||||
{searchQuery ? (
|
||||
<p className="members-empty-state__text">
|
||||
No results for <strong>{searchQuery}</strong>
|
||||
</p>
|
||||
) : (
|
||||
<p className="members-empty-state__text">No members found</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MembersTable({
|
||||
data,
|
||||
loading,
|
||||
total,
|
||||
currentPage,
|
||||
pageSize,
|
||||
searchQuery,
|
||||
onPageChange,
|
||||
onSortChange,
|
||||
}: MembersTableProps): JSX.Element {
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
const formatJoinedOn = (date: string | null): string => {
|
||||
if (!date) {
|
||||
return '—';
|
||||
}
|
||||
const d = new Date(date);
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
return '—';
|
||||
}
|
||||
return formatTimezoneAdjustedTimestamp(date, DATE_TIME_FORMATS.DASH_DATETIME);
|
||||
};
|
||||
|
||||
const columns: ColumnsType<MemberRow> = [
|
||||
{
|
||||
title: 'Name / Email',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (_, record): JSX.Element => (
|
||||
<NameEmailCell name={record.name} email={record.email} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Roles',
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
width: 180,
|
||||
render: (role: ROLES): JSX.Element => (
|
||||
<Badge color="vanilla">{formatRoleLabel(role)}</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Permissions',
|
||||
key: 'permissions',
|
||||
width: 250,
|
||||
render: (): JSX.Element => <span className="member-dash">―</span>,
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'right' as const,
|
||||
className: 'member-status-cell',
|
||||
sorter: (a, b): number => a.status.localeCompare(b.status),
|
||||
render: (status: MemberRow['status']): JSX.Element => (
|
||||
<StatusBadge status={status} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Joined On',
|
||||
dataIndex: 'joinedOn',
|
||||
key: 'joinedOn',
|
||||
width: 220,
|
||||
align: 'right' as const,
|
||||
sorter: (a, b): number => {
|
||||
if (!a.joinedOn && !b.joinedOn) {
|
||||
return 0;
|
||||
}
|
||||
if (!a.joinedOn) {
|
||||
return 1;
|
||||
}
|
||||
if (!b.joinedOn) {
|
||||
return -1;
|
||||
}
|
||||
return new Date(a.joinedOn).getTime() - new Date(b.joinedOn).getTime();
|
||||
},
|
||||
render: (joinedOn: string | null): JSX.Element => {
|
||||
const formatted = formatJoinedOn(joinedOn);
|
||||
const isDash = formatted === '—';
|
||||
return (
|
||||
<span className={isDash ? 'member-joined-dash' : 'member-joined-date'}>
|
||||
{formatted}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const showPaginationTotal = (_total: number, range: number[]): JSX.Element => (
|
||||
<>
|
||||
<span className="members-pagination-range">
|
||||
{range[0]} — {range[1]}
|
||||
</span>
|
||||
<span className="members-pagination-total"> of {_total}</span>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="members-table-wrapper">
|
||||
<Table<MemberRow>
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
rowClassName={(_, index): string =>
|
||||
index % 2 === 0 ? 'members-table-row--tinted' : ''
|
||||
}
|
||||
onChange={(_, __, sorter): void => {
|
||||
if (onSortChange) {
|
||||
onSortChange(
|
||||
sorter as SorterResult<MemberRow> | SorterResult<MemberRow>[],
|
||||
);
|
||||
}
|
||||
}}
|
||||
showSorterTooltip={false}
|
||||
locale={{
|
||||
emptyText: <MembersEmptyState searchQuery={searchQuery} />,
|
||||
}}
|
||||
className="members-table"
|
||||
/>
|
||||
{total > pageSize && (
|
||||
<Pagination
|
||||
current={currentPage}
|
||||
pageSize={pageSize}
|
||||
total={total}
|
||||
showTotal={showPaginationTotal}
|
||||
showSizeChanger={false}
|
||||
onChange={onPageChange}
|
||||
className="members-table-pagination"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MembersTable;
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { CaretDownOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import cx from 'classnames';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Tooltip } from 'antd';
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable react/function-component-definition */
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable react/function-component-definition */
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
@@ -65,6 +65,7 @@ function TestWrapper({ children }: { children: React.ReactNode }): JSX.Element {
|
||||
<Provider store={mockStore}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<VirtuosoMockContext.Provider
|
||||
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
||||
value={{ viewportHeight: 300, itemHeight: 40 }}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { uniqueOptions } from 'container/DashboardContainer/DashboardVariablesSelection/util';
|
||||
|
||||
import { OptionData } from './types';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/* eslint-disable react/require-default-props */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Input, InputProps, InputRef, Tooltip } from 'antd';
|
||||
import cx from 'classnames';
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
createContext,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
|
||||
@@ -78,10 +78,14 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const showAggregationInterval = useMemo(
|
||||
() => panelType !== PANEL_TYPES.VALUE,
|
||||
[panelType],
|
||||
);
|
||||
const showAggregationInterval = useMemo(() => {
|
||||
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
||||
if (panelType === PANEL_TYPES.VALUE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [panelType]);
|
||||
|
||||
const disableOperatorSelector =
|
||||
!queryAggregation.metricName || queryAggregation.metricName === '';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/require-default-props */
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Radio, RadioChangeEvent, Tooltip } from 'antd';
|
||||
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||
@@ -247,6 +248,8 @@ function QueryAddOns({
|
||||
filteredAddOns.some((addOn) => addOn.key === view.key),
|
||||
),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [panelType, isListViewPanel, query, showReduceTo]);
|
||||
|
||||
const handleOptionClick = (e: RadioChangeEvent): void => {
|
||||
|
||||
@@ -26,6 +26,7 @@ function QueryAggregationOptions({
|
||||
queryData: IBuilderQuery | IBuilderTraceOperator;
|
||||
}): JSX.Element {
|
||||
const showAggregationInterval = useMemo(() => {
|
||||
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
||||
if (panelType === PANEL_TYPES.VALUE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
/* eslint-disable no-cond-assign */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable class-methods-use-this */
|
||||
/* eslint-disable react/no-this-in-sfc */
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
@@ -145,6 +149,7 @@ const stopEventsExtension = EditorView.domEventHandlers({
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react/no-this-in-sfc
|
||||
function QueryAggregationSelect({
|
||||
onChange,
|
||||
queryData,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
/* eslint-disable react/require-default-props */
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { CheckCircleFilled } from '@ant-design/icons';
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
startCompletion,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { copilot } from '@uiw/codemirror-theme-copilot';
|
||||
import { githubLight } from '@uiw/codemirror-theme-github';
|
||||
@@ -84,7 +86,6 @@ interface QuerySearchProps {
|
||||
signalSource?: string;
|
||||
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
|
||||
onRun?: (query: string) => void;
|
||||
showFilterSuggestionsWithoutMetric?: boolean;
|
||||
}
|
||||
|
||||
function QuerySearch({
|
||||
@@ -95,7 +96,6 @@ function QuerySearch({
|
||||
onRun,
|
||||
signalSource,
|
||||
hardcodedAttributeKeys,
|
||||
showFilterSuggestionsWithoutMetric,
|
||||
}: QuerySearchProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
|
||||
@@ -252,8 +252,7 @@ function QuerySearch({
|
||||
async (searchText?: string): Promise<void> => {
|
||||
if (
|
||||
dataSource === DataSource.METRICS &&
|
||||
!queryData.aggregateAttribute?.key &&
|
||||
!showFilterSuggestionsWithoutMetric
|
||||
!queryData.aggregateAttribute?.key
|
||||
) {
|
||||
setKeySuggestions([]);
|
||||
return;
|
||||
@@ -302,7 +301,6 @@ function QuerySearch({
|
||||
queryData.aggregateAttribute?.key,
|
||||
signalSource,
|
||||
hardcodedAttributeKeys,
|
||||
showFilterSuggestionsWithoutMetric,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -391,6 +389,7 @@ function QuerySearch({
|
||||
|
||||
// Use callback to prevent dependency changes on each render
|
||||
const fetchValueSuggestions = useCallback(
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
async ({
|
||||
key,
|
||||
searchText,
|
||||
@@ -565,7 +564,15 @@ function QuerySearch({
|
||||
const lastPos = lastPosRef.current;
|
||||
|
||||
if (newPos.line !== lastPos.line || newPos.ch !== lastPos.ch) {
|
||||
setCursorPos(newPos);
|
||||
setCursorPos((lastPos) => {
|
||||
if (newPos.ch !== lastPos.ch && newPos.ch === 0) {
|
||||
Sentry.captureEvent({
|
||||
message: `Cursor jumped to start of line from ${lastPos.ch} to ${newPos.ch}`,
|
||||
level: 'warning',
|
||||
});
|
||||
}
|
||||
return newPos;
|
||||
});
|
||||
lastPosRef.current = newPos;
|
||||
|
||||
if (doc) {
|
||||
@@ -669,6 +676,7 @@ function QuerySearch({
|
||||
};
|
||||
|
||||
// Enhanced myCompletions function to better use context including query pairs
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function autoSuggestions(context: CompletionContext): CompletionResult | null {
|
||||
// This matches words before the cursor position
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
@@ -1086,6 +1094,7 @@ function QuerySearch({
|
||||
!(isLoadingSuggestions && lastKeyRef.current === keyName);
|
||||
|
||||
if (shouldFetch) {
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
debouncedFetchValueSuggestions({
|
||||
key: keyName,
|
||||
searchText,
|
||||
@@ -1556,7 +1565,6 @@ QuerySearch.defaultProps = {
|
||||
hardcodedAttributeKeys: undefined,
|
||||
placeholder:
|
||||
"Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')",
|
||||
showFilterSuggestionsWithoutMetric: false,
|
||||
};
|
||||
|
||||
export default QuerySearch;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/* eslint-disable react/require-default-props */
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
|
||||
import { Token } from 'antlr4';
|
||||
import TraceOperatorGrammarLexer from 'parser/TraceOperatorParser/TraceOperatorGrammarLexer';
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user