Compare commits

..

7 Commits

644 changed files with 2507 additions and 7640 deletions

View File

@@ -66,10 +66,9 @@ func runGenerateAuthz(_ context.Context) error {
registry := coretypes.NewRegistry()
allowedResources := map[string]bool{
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourcesServiceAccount).String(): true,
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourcesRole).String(): true,
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourcesRole).String(): true,
}
allowedTypes := map[string]bool{}

View File

@@ -18,19 +18,16 @@ import (
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/gateway"
"github.com/SigNoz/signoz/pkg/gateway/noopgateway"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
"github.com/SigNoz/signoz/pkg/meterreporter"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/retention"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/prometheus"
@@ -112,9 +109,6 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(_ licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]] {
return signoz.NewAuditorProviderFactories()
},
func(_ context.Context, _ factory.ProviderSettings, _ flagger.Flagger, _ licensing.Licensing, _ telemetrystore.TelemetryStore, _ retention.Getter, _ organization.Getter, _ zeus.Zeus) (factory.NamedMap[factory.ProviderFactory[meterreporter.Reporter, meterreporter.Config]], string) {
return signoz.NewMeterReporterProviderFactories(), "noop"
},
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
return querier.NewHandler(ps, q, a)
},

View File

@@ -1,55 +0,0 @@
package main
import (
"github.com/SigNoz/signoz/pkg/metercollector"
"github.com/SigNoz/signoz/pkg/telemetrylogs"
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
"github.com/SigNoz/signoz/pkg/telemetrytraces"
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
)
var meterConfigs = []metercollector.Config{
{
Provider: metercollector.ProviderStatic,
Static: metercollector.StaticConfig{
Name: zeustypes.MeterPlatformActive,
Unit: zeustypes.MeterUnitCount,
Aggregation: zeustypes.MeterAggregationMax,
Value: 1,
},
},
{
Provider: metercollector.ProviderTelemetry,
Telemetry: metercollector.TelemetryConfig{
Name: zeustypes.MeterLogSize,
Unit: zeustypes.MeterUnitBytes,
Aggregation: zeustypes.MeterAggregationSum,
DBName: telemetrylogs.DBName,
TableName: telemetrylogs.LogsV2LocalTableName,
DefaultRetentionDays: retentiontypes.DefaultLogsRetentionDays,
},
},
{
Provider: metercollector.ProviderTelemetry,
Telemetry: metercollector.TelemetryConfig{
Name: zeustypes.MeterSpanSize,
Unit: zeustypes.MeterUnitBytes,
Aggregation: zeustypes.MeterAggregationSum,
DBName: telemetrytraces.DBName,
TableName: telemetrytraces.SpanIndexV3LocalTableName,
DefaultRetentionDays: retentiontypes.DefaultTracesRetentionDays,
},
},
{
Provider: metercollector.ProviderTelemetry,
Telemetry: metercollector.TelemetryConfig{
Name: zeustypes.MeterDatapointCount,
Unit: zeustypes.MeterUnitCount,
Aggregation: zeustypes.MeterAggregationSum,
DBName: telemetrymetrics.DBName,
TableName: telemetrymetrics.SamplesV4LocalTableName,
DefaultRetentionDays: retentiontypes.DefaultMetricsRetentionDays,
},
},
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/spf13/cobra"
"github.com/SigNoz/signoz/cmd"
"github.com/SigNoz/signoz/ee/auditor/fileauditor"
"github.com/SigNoz/signoz/ee/auditor/otlphttpauditor"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
@@ -18,9 +17,6 @@ import (
"github.com/SigNoz/signoz/ee/gateway/httpgateway"
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
"github.com/SigNoz/signoz/ee/metercollector/staticmetercollector"
"github.com/SigNoz/signoz/ee/metercollector/telemetrymetercollector"
"github.com/SigNoz/signoz/ee/meterreporter/httpmeterreporter"
"github.com/SigNoz/signoz/ee/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/ee/modules/cloudintegration/implcloudintegration/implcloudprovider"
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
@@ -39,17 +35,14 @@ import (
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
pkgflagger "github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/gateway"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/meterreporter"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
pkgcloudintegration "github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/retention"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/prometheus"
@@ -162,25 +155,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
if err := factories.Add(otlphttpauditor.NewFactory(licensing, version.Info)); err != nil {
panic(err)
}
if err := factories.Add(fileauditor.NewFactory(licensing, version.Info)); err != nil {
panic(err)
}
return factories
},
func(ctx context.Context, providerSettings factory.ProviderSettings, flagger pkgflagger.Flagger, licensing licensing.Licensing, telemetryStore telemetrystore.TelemetryStore, retentionGetter retention.Getter, orgGetter organization.Getter, zeus zeus.Zeus) (factory.NamedMap[factory.ProviderFactory[meterreporter.Reporter, meterreporter.Config]], string) {
factories := signoz.NewMeterReporterProviderFactories()
collectorFactories := factory.MustNewNamedMap(
staticmetercollector.NewFactory(),
telemetrymetercollector.NewFactory(telemetryStore, retentionGetter),
)
if err := factories.Add(httpmeterreporter.NewFactory(collectorFactories, meterConfigs, flagger, licensing, orgGetter, zeus)); err != nil {
panic(err)
}
return factories, "http"
},
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
communityHandler := querier.NewHandler(ps, q, a)
return eequerier.NewHandler(ps, q, communityHandler)

View File

@@ -429,10 +429,3 @@ authz:
openfga:
# maximum tuples allowed per openfga write operation.
max_tuples_per_write: 300
##################### Meter Reporter #####################
meterreporter:
# The interval between collection ticks. Minimum 5m.
interval: 6h
# Whether to backfill sealed days from the license creation day.
backfill: true

View File

@@ -448,7 +448,6 @@ components:
- delete
- list
- assignee
- attach
type: string
AuthtypesRole:
properties:
@@ -2520,65 +2519,6 @@ components:
enabled:
type: boolean
type: object
InframonitoringtypesClusterRecord:
properties:
clusterCPU:
format: double
type: number
clusterCPUAllocatable:
format: double
type: number
clusterMemory:
format: double
type: number
clusterMemoryAllocatable:
format: double
type: number
clusterName:
type: string
meta:
additionalProperties:
type: string
nullable: true
type: object
nodeCountsByReadiness:
$ref: '#/components/schemas/InframonitoringtypesNodeCountsByReadiness'
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
required:
- clusterName
- clusterCPU
- clusterCPUAllocatable
- clusterMemory
- clusterMemoryAllocatable
- nodeCountsByReadiness
- podCountsByPhase
- meta
type: object
InframonitoringtypesClusters:
properties:
endTimeBeforeRetention:
type: boolean
records:
items:
$ref: '#/components/schemas/InframonitoringtypesClusterRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
$ref: '#/components/schemas/InframonitoringtypesResponseType'
warning:
$ref: '#/components/schemas/Querybuildertypesv5QueryWarnData'
required:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesHostFilter:
properties:
expression:
@@ -2658,54 +2598,6 @@ components:
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesNamespaceRecord:
properties:
meta:
additionalProperties:
type: string
nullable: true
type: object
namespaceCPU:
format: double
type: number
namespaceMemory:
format: double
type: number
namespaceName:
type: string
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
required:
- namespaceName
- namespaceCPU
- namespaceMemory
- podCountsByPhase
- meta
type: object
InframonitoringtypesNamespaces:
properties:
endTimeBeforeRetention:
type: boolean
records:
items:
$ref: '#/components/schemas/InframonitoringtypesNamespaceRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
$ref: '#/components/schemas/InframonitoringtypesResponseType'
warning:
$ref: '#/components/schemas/Querybuildertypesv5QueryWarnData'
required:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesNodeCondition:
enum:
- ready
@@ -2883,32 +2775,6 @@ components:
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesPostableClusters:
properties:
end:
format: int64
type: integer
filter:
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
nullable: true
type: array
limit:
type: integer
offset:
type: integer
orderBy:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
start:
format: int64
type: integer
required:
- start
- end
- limit
type: object
InframonitoringtypesPostableHosts:
properties:
end:
@@ -2935,32 +2801,6 @@ components:
- end
- limit
type: object
InframonitoringtypesPostableNamespaces:
properties:
end:
format: int64
type: integer
filter:
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
nullable: true
type: array
limit:
type: integer
offset:
type: integer
orderBy:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
start:
format: int64
type: integer
required:
- start
- end
- limit
type: object
InframonitoringtypesPostableNodes:
properties:
end:
@@ -9654,9 +9494,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:list
- ADMIN
- tokenizer:
- serviceaccount:list
- ADMIN
summary: List service accounts
tags:
- serviceaccount
@@ -9716,9 +9556,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:create
- ADMIN
- tokenizer:
- serviceaccount:create
- ADMIN
summary: Create service account
tags:
- serviceaccount
@@ -9766,9 +9606,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:delete
- ADMIN
- tokenizer:
- serviceaccount:delete
- ADMIN
summary: Deletes a service account
tags:
- serviceaccount
@@ -9823,9 +9663,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:read
- ADMIN
- tokenizer:
- serviceaccount:read
- ADMIN
summary: Gets a service account
tags:
- serviceaccount
@@ -9883,9 +9723,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:update
- ADMIN
- tokenizer:
- serviceaccount:update
- ADMIN
summary: Updates a service account
tags:
- serviceaccount
@@ -9937,9 +9777,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:read
- ADMIN
- tokenizer:
- serviceaccount:read
- ADMIN
summary: List service account keys
tags:
- serviceaccount
@@ -10005,9 +9845,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:update
- ADMIN
- tokenizer:
- serviceaccount:update
- ADMIN
summary: Create a service account key
tags:
- serviceaccount
@@ -10060,9 +9900,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:update
- ADMIN
- tokenizer:
- serviceaccount:update
- ADMIN
summary: Revoke a service account key
tags:
- serviceaccount
@@ -10125,9 +9965,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:update
- ADMIN
- tokenizer:
- serviceaccount:update
- ADMIN
summary: Updates a service account key
tags:
- serviceaccount
@@ -10186,9 +10026,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:read
- ADMIN
- tokenizer:
- serviceaccount:read
- ADMIN
summary: Gets service account roles
tags:
- serviceaccount
@@ -10248,11 +10088,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:attach
- role:attach
- ADMIN
- tokenizer:
- serviceaccount:attach
- role:attach
- ADMIN
summary: Create service account role
tags:
- serviceaccount
@@ -10299,11 +10137,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:attach
- role:attach
- ADMIN
- tokenizer:
- serviceaccount:attach
- role:attach
- ADMIN
summary: Delete service account role
tags:
- serviceaccount
@@ -11831,77 +11667,6 @@ paths:
summary: Health check
tags:
- health
/api/v2/infra_monitoring/clusters:
post:
deprecated: false
description: 'Returns a paginated list of Kubernetes clusters with key aggregated
metrics derived by summing per-node values within the group: CPU usage, CPU
allocatable, memory working set, memory allocatable. Each row also reports
per-group nodeCountsByReadiness ({ ready, notReady } from each node''s latest
k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending,
running, succeeded, failed, unknown } from each pod''s latest k8s.pod.phase
value). Each cluster includes metadata attributes (k8s.cluster.name). The
response type is ''list'' for the default k8s.cluster.name grouping or ''grouped_list''
for custom groupBy keys; in both modes every row aggregates nodes and pods
in the group. Supports filtering via a filter expression, custom groupBy,
ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination
via offset/limit. Also reports missing required metrics and whether the requested
time range falls before the data retention boundary. Numeric metric fields
(clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable)
return -1 as a sentinel when no data is available for that field.'
operationId: ListClusters
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InframonitoringtypesPostableClusters'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/InframonitoringtypesClusters'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List Clusters for Infra Monitoring
tags:
- inframonitoring
/api/v2/infra_monitoring/hosts:
post:
deprecated: false
@@ -11970,74 +11735,6 @@ paths:
summary: List Hosts for Infra Monitoring
tags:
- inframonitoring
/api/v2/infra_monitoring/namespaces:
post:
deprecated: false
description: 'Returns a paginated list of Kubernetes namespaces with key aggregated
pod metrics: CPU usage and memory working set (summed across pods in the group),
plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown
} from each pod''s latest k8s.pod.phase value in the window). Each namespace
includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response
type is ''list'' for the default k8s.namespace.name grouping or ''grouped_list''
for custom groupBy keys; in both modes every row aggregates pods in the group.
Supports filtering via a filter expression, custom groupBy, ordering by cpu
/ memory, and pagination via offset/limit. Also reports missing required metrics
and whether the requested time range falls before the data retention boundary.
Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel
when no data is available for that field.'
operationId: ListNamespaces
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InframonitoringtypesPostableNamespaces'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/InframonitoringtypesNamespaces'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List Namespaces for Infra Monitoring
tags:
- inframonitoring
/api/v2/infra_monitoring/nodes:
post:
deprecated: false

View File

@@ -1,33 +0,0 @@
package fileauditor
import (
"context"
"log/slog"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/audittypes"
)
func (provider *provider) export(ctx context.Context, events []audittypes.AuditEvent) error {
logs := audittypes.NewPLogsFromAuditEvents(events, "signoz", provider.build.Version(), "signoz.audit")
payload, err := provider.marshaler.MarshalLogs(logs)
if err != nil {
return err
}
// Combine the payload and trailing newline into one Write call so the line
// is emitted in a single syscall — concurrent readers see either the full
// line or nothing, never a torn JSON object.
payload = append(payload, '\n')
provider.mu.Lock()
defer provider.mu.Unlock()
if _, err := provider.file.Write(payload); err != nil {
provider.settings.Logger().ErrorContext(ctx, "audit export failed", errors.Attr(err), slog.Int("dropped_log_records", len(events)))
return err
}
return provider.file.Sync()
}

View File

@@ -1,100 +0,0 @@
package fileauditor
import (
"context"
"os"
"sync"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/auditor/auditorserver"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/version"
"go.opentelemetry.io/collector/pdata/plog"
)
var _ auditor.Auditor = (*provider)(nil)
type provider struct {
settings factory.ScopedProviderSettings
config auditor.Config
licensing licensing.Licensing
build version.Build
server *auditorserver.Server
marshaler plog.JSONMarshaler
file *os.File
mu sync.Mutex
}
func NewFactory(licensing licensing.Licensing, build version.Build) factory.ProviderFactory[auditor.Auditor, auditor.Config] {
return factory.NewProviderFactory(factory.MustNewName("file"), func(ctx context.Context, providerSettings factory.ProviderSettings, config auditor.Config) (auditor.Auditor, error) {
return newProvider(ctx, providerSettings, config, licensing, build)
})
}
func newProvider(_ context.Context, providerSettings factory.ProviderSettings, config auditor.Config, licensing licensing.Licensing, build version.Build) (auditor.Auditor, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/auditor/fileauditor")
file, err := os.OpenFile(config.File.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, auditor.ErrCodeAuditExportFailed, "failed to open audit file %q", config.File.Path)
}
provider := &provider{
settings: settings,
config: config,
licensing: licensing,
build: build,
marshaler: plog.JSONMarshaler{},
file: file,
}
server, err := auditorserver.New(settings,
auditorserver.Config{
BufferSize: config.BufferSize,
BatchSize: config.BatchSize,
FlushInterval: config.FlushInterval,
},
provider.export,
)
if err != nil {
_ = file.Close()
return nil, err
}
provider.server = server
return provider, nil
}
func (provider *provider) Start(ctx context.Context) error {
return provider.server.Start(ctx)
}
func (provider *provider) Audit(ctx context.Context, event audittypes.AuditEvent) {
if event.PrincipalAttributes.PrincipalOrgID.IsZero() {
provider.settings.Logger().WarnContext(ctx, "audit event dropped as org_id is zero")
return
}
if _, err := provider.licensing.GetActive(ctx, event.PrincipalAttributes.PrincipalOrgID); err != nil {
return
}
provider.server.Add(ctx, event)
}
func (provider *provider) Healthy() <-chan struct{} {
return provider.server.Healthy()
}
func (provider *provider) Stop(ctx context.Context) error {
serverErr := provider.server.Stop(ctx)
fileErr := provider.file.Close()
if serverErr != nil {
return serverErr
}
return fileErr
}

View File

@@ -1,61 +0,0 @@
// Package staticmetercollector emits a fixed-value meter reading per org per window.
package staticmetercollector
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/metercollector"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
var _ metercollector.MeterCollector = (*Provider)(nil)
type Provider struct {
settings factory.ScopedProviderSettings
config metercollector.StaticConfig
}
func NewFactory() factory.ProviderFactory[metercollector.MeterCollector, metercollector.Config] {
return factory.NewProviderFactory(factory.MustNewName(metercollector.ProviderStatic), func(ctx context.Context, providerSettings factory.ProviderSettings, config metercollector.Config) (metercollector.MeterCollector, error) {
return newProvider(providerSettings, config.Static), nil
},
)
}
func newProvider(providerSettings factory.ProviderSettings, config metercollector.StaticConfig) *Provider {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/metercollector/staticmetercollector")
return &Provider{
settings: settings,
config: config,
}
}
func (provider *Provider) Name() zeustypes.MeterName { return provider.config.Name }
func (provider *Provider) Unit() zeustypes.MeterUnit { return provider.config.Unit }
func (provider *Provider) Aggregation() zeustypes.MeterAggregation {
return provider.config.Aggregation
}
func (provider *Provider) Origin(_ context.Context, _ valuer.UUID, license *licensetypes.License, _ time.Time) (time.Time, error) {
if license == nil || license.CreatedAt.IsZero() {
return time.Time{}, nil
}
createdAt := license.CreatedAt.UTC()
return time.Date(createdAt.Year(), createdAt.Month(), createdAt.Day(), 0, 0, 0, 0, time.UTC), nil
}
func (provider *Provider) Collect(_ context.Context, orgID valuer.UUID, license *licensetypes.License, window zeustypes.MeterWindow) ([]zeustypes.Meter, error) {
if license == nil || license.Key == "" {
return nil, nil
}
return []zeustypes.Meter{
zeustypes.NewMeter(provider.config.Name, provider.config.Value, provider.config.Unit, provider.config.Aggregation, window, zeustypes.NewDimensions(zeustypes.OrganizationID.String(orgID.StringValue()))),
}, nil
}

View File

@@ -1,247 +0,0 @@
// Package telemetrymetercollector collects telemetry meters (logs, traces, metrics)
// by retention. One Provider materializes per TelemetryConfig.
package telemetrymetercollector
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"time"
"github.com/huandu/go-sqlbuilder"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/metercollector"
"github.com/SigNoz/signoz/pkg/modules/retention"
"github.com/SigNoz/signoz/pkg/telemetrymeter"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
var (
labelKeyPattern = regexp.MustCompile(`^[A-Za-z0-9_.\-]+$`)
labelValuePattern = regexp.MustCompile(`^[A-Za-z0-9_.\-:]+$`)
)
var _ metercollector.MeterCollector = (*Provider)(nil)
type Provider struct {
settings factory.ScopedProviderSettings
config metercollector.TelemetryConfig
telemetryStore telemetrystore.TelemetryStore
retentionGetter retention.Getter
}
func NewFactory(telemetryStore telemetrystore.TelemetryStore, retentionGetter retention.Getter) factory.ProviderFactory[metercollector.MeterCollector, metercollector.Config] {
return factory.NewProviderFactory(factory.MustNewName(metercollector.ProviderTelemetry), func(ctx context.Context, providerSettings factory.ProviderSettings, config metercollector.Config) (metercollector.MeterCollector, error) {
return newProvider(providerSettings, config.Telemetry, telemetryStore, retentionGetter), nil
},
)
}
func newProvider(
providerSettings factory.ProviderSettings,
config metercollector.TelemetryConfig,
telemetryStore telemetrystore.TelemetryStore,
retentionGetter retention.Getter,
) *Provider {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/metercollector/telemetrymetercollector")
return &Provider{
settings: settings,
config: config,
telemetryStore: telemetryStore,
retentionGetter: retentionGetter,
}
}
func (provider *Provider) Name() zeustypes.MeterName { return provider.config.Name }
func (provider *Provider) Unit() zeustypes.MeterUnit { return provider.config.Unit }
func (provider *Provider) Aggregation() zeustypes.MeterAggregation {
return provider.config.Aggregation
}
func (provider *Provider) Origin(ctx context.Context, _ valuer.UUID, _ *licensetypes.License, todayStart time.Time) (time.Time, error) {
query, args := buildOriginQuery(provider.config.Name.String())
var minMs int64
if err := provider.telemetryStore.ClickhouseDB().QueryRow(ctx, query, args...).Scan(&minMs); err != nil {
return time.Time{}, err
}
if minMs == 0 {
return todayStart, nil
}
minDay := time.UnixMilli(minMs).UTC()
return time.Date(minDay.Year(), minDay.Month(), minDay.Day(), 0, 0, 0, 0, time.UTC), nil
}
func (provider *Provider) Collect(
ctx context.Context,
orgID valuer.UUID,
_ *licensetypes.License,
window zeustypes.MeterWindow,
) ([]zeustypes.Meter, error) {
meterName := provider.config.Name.String()
segments, err := provider.retentionGetter.GetRetentionPolicySegments(
ctx,
orgID,
provider.config.DBName,
provider.config.TableName,
provider.config.DefaultRetentionDays,
window.StartUnixMilli,
window.EndUnixMilli,
)
if err != nil {
return nil, err
}
valuesByRetentionDays := make(map[int]int64)
for _, segment := range segments {
query, args, err := buildQuery(meterName, segment)
if err != nil {
return nil, err
}
rows, err := provider.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, err
}
if err := func() error {
defer rows.Close()
for rows.Next() {
var retentionDays int32
var value int64
if err := rows.Scan(&retentionDays, &value); err != nil {
return err
}
valuesByRetentionDays[int(retentionDays)] += value
}
if err := rows.Err(); err != nil {
return err
}
return nil
}(); err != nil {
return nil, err
}
}
meters := make([]zeustypes.Meter, 0, len(valuesByRetentionDays))
for retentionDays, value := range valuesByRetentionDays {
meters = append(meters, zeustypes.NewMeter(provider.config.Name, value, provider.config.Unit, provider.config.Aggregation, window, buildDimensions(orgID, retentionDays)))
}
// Empty windows still emit a sentinel so checkpoints can advance.
if len(meters) == 0 && len(segments) > 0 {
meters = append(meters, zeustypes.NewMeter(provider.config.Name, 0, provider.config.Unit, provider.config.Aggregation, window, buildDimensions(orgID, segments[len(segments)-1].DefaultDays)))
}
return meters, nil
}
func buildOriginQuery(meterName string) (string, []any) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select("toInt64(ifNull(min(unix_milli), 0))")
sb.From(telemetrymeter.DBName + "." + telemetrymeter.SamplesTableName)
sb.Where(sb.Equal("metric_name", meterName))
return sb.BuildWithFlavor(sqlbuilder.ClickHouse)
}
func buildQuery(meterName string, segment *retentiontypes.RetentionPolicySegment) (string, []any, error) {
retentionExpr, err := buildRetentionMultiIfSQL(segment.Rules, segment.DefaultDays)
if err != nil {
return "", nil, err
}
selects := []string{
retentionExpr + " AS retention_days",
"toInt64(ifNull(sum(value), 0)) AS value",
}
sb := sqlbuilder.NewSelectBuilder()
sb.Select(selects...)
sb.From(telemetrymeter.DBName + "." + telemetrymeter.SamplesTableName)
sb.Where(
sb.Equal("metric_name", meterName),
sb.GTE("unix_milli", segment.StartMs),
sb.LT("unix_milli", segment.EndMs),
)
sb.GroupBy("retention_days")
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return query, args, nil
}
func buildRetentionMultiIfSQL(rules []retentiontypes.CustomRetentionRule, defaultDays int) (string, error) {
if defaultDays <= 0 {
return "", errors.Newf(errors.TypeInvalidInput, metercollector.ErrCodeMeterCollectorInvalidCustomRetentionRule, "non-positive default retention %d", defaultDays)
}
if len(rules) == 0 {
return "toInt32(" + strconv.Itoa(defaultDays) + ")", nil
}
arms := make([]string, 0, 2*len(rules)+1)
for ruleIndex, rule := range rules {
if rule.TTLDays <= 0 {
return "", errors.Newf(errors.TypeInvalidInput, metercollector.ErrCodeMeterCollectorInvalidCustomRetentionRule, "rule %d has non-positive ttl_days %d", ruleIndex, rule.TTLDays)
}
conditionExpr, err := buildRuleConditionSQL(ruleIndex, rule)
if err != nil {
return "", err
}
arms = append(arms, conditionExpr)
arms = append(arms, strconv.Itoa(rule.TTLDays))
}
arms = append(arms, strconv.Itoa(defaultDays))
return "toInt32(multiIf(" + strings.Join(arms, ", ") + "))", nil
}
func buildRuleConditionSQL(ruleIndex int, rule retentiontypes.CustomRetentionRule) (string, error) {
if len(rule.Filters) == 0 {
return "", errors.Newf(errors.TypeInvalidInput, metercollector.ErrCodeMeterCollectorInvalidCustomRetentionRule, "rule %d has no filters", ruleIndex)
}
filterExprs := make([]string, 0, len(rule.Filters))
for filterIndex, filter := range rule.Filters {
if !labelKeyPattern.MatchString(filter.Key) {
return "", errors.Newf(errors.TypeInvalidInput, metercollector.ErrCodeMeterCollectorInvalidCustomRetentionRule, "rule %d filter %d has invalid key %q", ruleIndex, filterIndex, filter.Key)
}
if len(filter.Values) == 0 {
return "", errors.Newf(errors.TypeInvalidInput, metercollector.ErrCodeMeterCollectorInvalidCustomRetentionRule, "rule %d filter %d has no values", ruleIndex, filterIndex)
}
quoted := make([]string, len(filter.Values))
for valueIndex, value := range filter.Values {
if !labelValuePattern.MatchString(value) {
return "", errors.Newf(errors.TypeInvalidInput, metercollector.ErrCodeMeterCollectorInvalidCustomRetentionRule, "rule %d filter %d value %d is invalid %q", ruleIndex, filterIndex, valueIndex, value)
}
quoted[valueIndex] = "'" + value + "'"
}
filterExprs = append(filterExprs, fmt.Sprintf("JSONExtractString(labels, '%s') IN (%s)", filter.Key, strings.Join(quoted, ", ")))
}
return strings.Join(filterExprs, " AND "), nil
}
func buildDimensions(orgID valuer.UUID, retentionDays int) map[string]string {
retentionDurationSeconds := int64(retentionDays) * 24 * 60 * 60 // seconds
return zeustypes.NewDimensions(
zeustypes.OrganizationID.String(orgID.StringValue()),
zeustypes.RetentionDuration.String(strconv.FormatInt(retentionDurationSeconds, 10)),
)
}

View File

@@ -1,318 +0,0 @@
package httpmeterreporter
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/metercollector"
"github.com/SigNoz/signoz/pkg/meterreporter"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/zeus"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
)
var _ factory.ServiceWithHealthy = (*Provider)(nil)
type Provider struct {
settings factory.ScopedProviderSettings
config meterreporter.Config
collectorsByName map[zeustypes.MeterName]metercollector.MeterCollector
flagger flagger.Flagger
licensing licensing.Licensing
orgGetter organization.Getter
zeus zeus.Zeus
healthyC chan struct{}
stopC chan struct{}
metrics *reporterMetrics
}
func NewFactory(collectorFactories factory.NamedMap[factory.ProviderFactory[metercollector.MeterCollector, metercollector.Config]], collectorConfigs []metercollector.Config, flagger flagger.Flagger, licensing licensing.Licensing, orgGetter organization.Getter, zeus zeus.Zeus) factory.ProviderFactory[meterreporter.Reporter, meterreporter.Config] {
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, providerSettings factory.ProviderSettings, config meterreporter.Config) (meterreporter.Reporter, error) {
return newProvider(ctx, providerSettings, config, collectorFactories, collectorConfigs, flagger, licensing, orgGetter, zeus)
},
)
}
func newProvider(
ctx context.Context,
providerSettings factory.ProviderSettings,
config meterreporter.Config,
collectorFactories factory.NamedMap[factory.ProviderFactory[metercollector.MeterCollector, metercollector.Config]],
collectorConfigs []metercollector.Config,
flagger flagger.Flagger,
licensing licensing.Licensing,
orgGetter organization.Getter,
zeus zeus.Zeus,
) (*Provider, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/meterreporter/httpmeterreporter")
collectorsByName := map[zeustypes.MeterName]metercollector.MeterCollector{}
for _, collectorConfig := range collectorConfigs {
collector, err := factory.NewProviderFromNamedMap(ctx, providerSettings, collectorConfig, collectorFactories, collectorConfig.Provider)
if err != nil {
return nil, err
}
if _, exists := collectorsByName[collector.Name()]; exists {
return nil, errors.Newf(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "duplicate meter collector %q", collector.Name())
}
collectorsByName[collector.Name()] = collector
}
metrics, err := newReporterMetrics(settings.Meter())
if err != nil {
return nil, err
}
return &Provider{
settings: settings,
config: config,
collectorsByName: collectorsByName,
flagger: flagger,
licensing: licensing,
orgGetter: orgGetter,
zeus: zeus,
healthyC: make(chan struct{}),
stopC: make(chan struct{}),
metrics: metrics,
}, nil
}
func (provider *Provider) Start(ctx context.Context) error {
close(provider.healthyC)
provider.collect(ctx)
ticker := time.NewTicker(provider.config.Interval)
defer ticker.Stop()
for {
select {
case <-provider.stopC:
return nil
case <-ticker.C:
provider.collect(ctx)
}
}
}
func (provider *Provider) collect(ctx context.Context) {
ctx, span := provider.settings.Tracer().Start(ctx, "meterreporter.Collect", trace.WithAttributes(attribute.String("meterreporter.provider", "http")))
defer span.End()
orgs, err := provider.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil {
span.RecordError(err)
provider.settings.Logger().ErrorContext(ctx, "failed to get orgs data", errors.Attr(err))
return
}
for _, org := range orgs {
evalCtx := featuretypes.NewFlaggerEvaluationContext(org.ID)
if !provider.flagger.BooleanOrEmpty(ctx, flagger.FeatureUseMeterReporter, evalCtx) {
provider.settings.Logger().DebugContext(ctx, "meter reporter disabled for org, skipping reporting", slog.String("org_id", org.ID.StringValue()))
continue
}
license, err := provider.licensing.GetActive(ctx, org.ID)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
provider.settings.Logger().DebugContext(ctx, "no active license found for org, skipping reporting", slog.String("org_id", org.ID.StringValue()))
continue
}
span.RecordError(err)
provider.settings.Logger().ErrorContext(ctx, "failed to fetch active license for org", errors.Attr(err), slog.String("org_id", org.ID.StringValue()))
return
}
if err := provider.collectOrg(ctx, org, license); err != nil {
span.RecordError(err)
provider.settings.Logger().ErrorContext(ctx, "failed to collect meters", errors.Attr(err), slog.String("org_id", org.ID.StringValue()))
}
}
}
func (provider *Provider) Stop(ctx context.Context) error {
close(provider.stopC)
return nil
}
func (provider *Provider) Healthy() <-chan struct{} {
return provider.healthyC
}
func (provider *Provider) collectOrg(ctx context.Context, org *types.Organization, license *licensetypes.License) error {
now := time.Now().UTC()
// Use one timestamp so a tick cannot straddle midnight.
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
if provider.config.Backfill {
checkpointsByMeter, err := provider.checkpoints(ctx, license.Key)
if err != nil {
return err
}
nextByCollector := provider.nextDays(license, todayStart, checkpointsByMeter)
start, end, ok := backfillRange(nextByCollector, todayStart)
if ok {
for day := start; !day.After(end); day = day.AddDate(0, 0, 1) {
eligible := eligibleCollectors(provider.collectorsByName, nextByCollector, day)
if len(eligible) == 0 {
continue
}
window, err := zeustypes.NewMeterWindow(day.UnixMilli(), day.AddDate(0, 0, 1).UnixMilli(), true)
if err != nil {
return err
}
if err := provider.report(ctx, org.ID, license, window, eligible); err != nil {
provider.settings.Logger().WarnContext(ctx, "failed to backfill for day", errors.Attr(err), slog.String("date", day.Format("2006-01-02")))
return err
}
}
}
}
// Today's partial window: every collector is always eligible (next <= today).
if now.UnixMilli() > todayStart.UnixMilli() {
todayWindow, err := zeustypes.NewMeterWindow(todayStart.UnixMilli(), now.UnixMilli(), false)
if err != nil {
return err
}
return provider.report(ctx, org.ID, license, todayWindow, provider.collectorsByName)
}
return nil
}
func (provider *Provider) checkpoints(ctx context.Context, licenseKey string) (map[string]time.Time, error) {
list, err := provider.zeus.ListMeterCheckpoints(ctx, licenseKey)
if err != nil {
provider.metrics.checkpoints.Add(ctx, 1, metric.WithAttributes(errors.TypeAttr(err)))
return nil, err
}
provider.metrics.checkpoints.Add(ctx, 1)
checkpointsByMeter := make(map[string]time.Time, len(list))
for _, checkpoint := range list {
checkpointsByMeter[checkpoint.Name] = checkpoint.StartDate.UTC()
}
return checkpointsByMeter, nil
}
func (provider *Provider) nextDays(license *licensetypes.License, todayStart time.Time, checkpointsByMeter map[string]time.Time) map[zeustypes.MeterName]time.Time {
nextByCollector := make(map[zeustypes.MeterName]time.Time, len(provider.collectorsByName))
licenseCreatedAt := license.CreatedAt.UTC()
licenseCreatedAtDay := time.Date(licenseCreatedAt.Year(), licenseCreatedAt.Month(), licenseCreatedAt.Day(), 0, 0, 0, 0, time.UTC)
for _, collector := range provider.collectorsByName {
checkpoint, hasCheckpoint := checkpointsByMeter[collector.Name().String()]
nextByCollector[collector.Name()] = nextReportableDay(licenseCreatedAtDay, todayStart, checkpoint, hasCheckpoint)
}
return nextByCollector
}
func nextReportableDay(licenseCreatedAtDay time.Time, todayStart time.Time, checkpoint time.Time, hasCheckpoint bool) time.Time {
next := licenseCreatedAtDay
if next.IsZero() {
next = todayStart
}
if hasCheckpoint {
checkpointNext := checkpoint.AddDate(0, 0, 1)
if checkpointNext.After(next) {
next = checkpointNext
}
}
return next
}
func (provider *Provider) report(ctx context.Context, orgID valuer.UUID, license *licensetypes.License, window zeustypes.MeterWindow, collectors map[zeustypes.MeterName]metercollector.MeterCollector) error {
date := time.UnixMilli(window.StartUnixMilli).UTC().Format("2006-01-02")
meters := make([]zeustypes.Meter, 0, len(collectors))
for _, collector := range collectors {
meterAttr := attribute.String("signoz.meter.name", collector.Name().String())
collectedReadings, err := collector.Collect(ctx, orgID, license, window)
if err != nil {
provider.metrics.collections.Add(ctx, 1, metric.WithAttributes(meterAttr, errors.TypeAttr(err)))
continue
}
provider.metrics.collections.Add(ctx, 1, metric.WithAttributes(meterAttr))
meters = append(meters, collectedReadings...)
}
if len(meters) == 0 {
return nil
}
idempotencyKey := fmt.Sprintf("meterreporter:%s", date)
body, err := json.Marshal(meters)
if err != nil {
provider.metrics.reports.Add(ctx, 1, metric.WithAttributes(errors.TypeAttr(err)))
return err
}
if err := provider.zeus.PutMetersV3(ctx, license.Key, idempotencyKey, body); err != nil {
provider.metrics.reports.Add(ctx, 1, metric.WithAttributes(errors.TypeAttr(err)))
return err
}
provider.metrics.reports.Add(ctx, 1)
provider.metrics.meters.Add(ctx, int64(len(meters)))
return nil
}
// backfillRange returns the inclusive sealed-day range ending at yesterday.
func backfillRange(nextByCollector map[zeustypes.MeterName]time.Time, todayStart time.Time) (start, end time.Time, ok bool) {
yesterday := todayStart.AddDate(0, 0, -1)
for _, next := range nextByCollector {
if !next.Before(todayStart) {
continue
}
if start.IsZero() || next.Before(start) {
start = next
}
}
if start.IsZero() || start.After(yesterday) {
return time.Time{}, time.Time{}, false
}
return start, yesterday, true
}
func eligibleCollectors(collectors map[zeustypes.MeterName]metercollector.MeterCollector, nextByCollector map[zeustypes.MeterName]time.Time, day time.Time) map[zeustypes.MeterName]metercollector.MeterCollector {
eligible := make(map[zeustypes.MeterName]metercollector.MeterCollector, len(collectors))
for name, collector := range collectors {
if !nextByCollector[name].After(day) {
eligible[name] = collector
}
}
return eligible
}

View File

@@ -1,48 +0,0 @@
package httpmeterreporter
import (
"github.com/SigNoz/signoz/pkg/errors"
"go.opentelemetry.io/otel/metric"
)
type reporterMetrics struct {
checkpoints metric.Int64Counter
reports metric.Int64Counter
collections metric.Int64Counter
meters metric.Int64Counter
}
func newReporterMetrics(meter metric.Meter) (*reporterMetrics, error) {
var errs error
checkpoints, err := meter.Int64Counter("signoz.meterreporter.checkpoints", metric.WithDescription("Zeus meter checkpoint fetches."), metric.WithUnit("{checkpoint}"))
if err != nil {
errs = errors.Join(errs, err)
}
reports, err := meter.Int64Counter("signoz.meterreporter.reports", metric.WithDescription("Meter reports shipped to Zeus."), metric.WithUnit("{report}"))
if err != nil {
errs = errors.Join(errs, err)
}
collections, err := meter.Int64Counter("signoz.meterreporter.collections", metric.WithDescription("Per-meter collect calls."), metric.WithUnit("{collection}"))
if err != nil {
errs = errors.Join(errs, err)
}
meters, err := meter.Int64Counter("signoz.meterreporter.meters", metric.WithDescription("Meter readings shipped to Zeus."), metric.WithUnit("{meter}"))
if err != nil {
errs = errors.Join(errs, err)
}
if errs != nil {
return nil, errs
}
return &reporterMetrics{
checkpoints: checkpoints,
reports: reports,
collections: collections,
meters: meters,
}, nil
}

View File

@@ -150,72 +150,6 @@ func (provider *Provider) PutMetersV2(ctx context.Context, key string, data []by
return err
}
func (provider *Provider) PutMetersV3(ctx context.Context, key string, idempotencyKey string, data []byte) error {
headers := http.Header{}
if idempotencyKey != "" {
headers.Set("X-Idempotency-Key", idempotencyKey)
}
_, err := provider.doWithHeaders(
ctx,
provider.config.URL.JoinPath("/v2/meters"),
http.MethodPost,
key,
data,
headers,
)
return err
}
func (provider *Provider) ListMeterCheckpoints(ctx context.Context, key string) ([]zeustypes.MeterCheckpoint, error) {
response, err := provider.do(
ctx,
provider.config.URL.JoinPath("/v2/meters/checkpoints"),
http.MethodGet,
key,
nil,
)
if err != nil {
return nil, err
}
checkpointValues := gjson.GetBytes(response, "data")
if !checkpointValues.Exists() || checkpointValues.Type == gjson.Null {
return nil, errors.Newf(errors.TypeInternal, zeus.ErrCodeResponseMalformed, "meter checkpoints are required")
}
if !checkpointValues.IsArray() {
return nil, errors.Newf(errors.TypeInternal, zeus.ErrCodeResponseMalformed, "meter checkpoints must be an array")
}
checkpointResults := checkpointValues.Array()
checkpoints := make([]zeustypes.MeterCheckpoint, 0, len(checkpointResults))
for _, checkpointValue := range checkpointResults {
name := checkpointValue.Get("name").String()
if name == "" {
return nil, errors.Newf(errors.TypeInternal, zeus.ErrCodeResponseMalformed, "meter checkpoint name is required")
}
startDateString := checkpointValue.Get("start_date").String()
if startDateString == "" {
return nil, errors.Newf(errors.TypeInternal, zeus.ErrCodeResponseMalformed, "meter checkpoint start_date is required for %q", name)
}
startDate, err := time.Parse("2006-01-02", startDateString)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, zeus.ErrCodeResponseMalformed, "parse meter checkpoint start_date %q for %q", startDateString, name)
}
checkpoints = append(checkpoints, zeustypes.MeterCheckpoint{
Name: name,
StartDate: startDate,
})
}
return checkpoints, nil
}
func (provider *Provider) PutProfile(ctx context.Context, key string, profile *zeustypes.PostableProfile) error {
body, err := json.Marshal(profile)
if err != nil {
@@ -251,21 +185,12 @@ func (provider *Provider) PutHost(ctx context.Context, key string, host *zeustyp
}
func (provider *Provider) do(ctx context.Context, url *url.URL, method string, key string, requestBody []byte) ([]byte, error) {
return provider.doWithHeaders(ctx, url, method, key, requestBody, nil)
}
func (provider *Provider) doWithHeaders(ctx context.Context, url *url.URL, method string, key string, requestBody []byte, extraHeaders http.Header) ([]byte, error) {
request, err := http.NewRequestWithContext(ctx, method, url.String(), bytes.NewBuffer(requestBody))
if err != nil {
return nil, err
}
request.Header.Set("X-Signoz-Cloud-Api-Key", key)
request.Header.Set("Content-Type", "application/json")
for k, vs := range extraHeaders {
for _, v := range vs {
request.Header.Add(k, v)
}
}
response, err := provider.httpClient.Do(request)
if err != nil {

View File

@@ -28,7 +28,7 @@ const config: Config.InitialOptions = {
'<rootDir>/node_modules/@signozhq/icons/dist/index.esm.js',
'^react-syntax-highlighter/dist/esm/(.*)$':
'<rootDir>/node_modules/react-syntax-highlighter/dist/cjs/$1',
'^@signozhq/(?!ui(?:/|$))([^/]+)$':
'^@signozhq/(?!ui$)([^/]+)$':
'<rootDir>/node_modules/@signozhq/$1/dist/$1.js',
},
extensionsToTreatAsEsm: ['.ts'],

View File

@@ -50,7 +50,7 @@
"@signozhq/design-tokens": "2.1.4",
"@signozhq/icons": "0.1.0",
"@signozhq/resizable": "0.0.2",
"@signozhq/ui": "0.0.18",
"@signozhq/ui": "0.0.17",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",
"@uiw/codemirror-theme-copilot": "4.23.11",

View File

@@ -165,6 +165,8 @@ function createMockAppContext(
orgPreferences: createMockOrgPreferences(),
userPreferences: [],
isLoggedIn: true,
isNoAuthMode: false,
isPreflightLoading: false,
org: [{ createdAt: 0, id: 'org-id', displayName: 'Test Org' }],
isFetchingUser: false,
isFetchingActiveLicense: false,

View File

@@ -58,6 +58,7 @@ function App(): JSX.Element {
isLoggedIn: isLoggedInState,
featureFlags,
org,
isPreflightLoading,
} = useAppContext();
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
@@ -355,6 +356,10 @@ function App(): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isCloudUser, isEnterpriseSelfHostedUser]);
if (isPreflightLoading) {
return <Spinner tip="Loading..." />;
}
// if the user is in logged in state
if (isLoggedInState) {
// if the setup calls are loading then return a spinner

View File

@@ -0,0 +1,72 @@
import axios from 'axios';
import { getIsNoAuthMode } from 'utils/noAuthMode';
import { interceptorRejected } from '../index';
jest.mock('utils/noAuthMode', () => ({
getIsNoAuthMode: jest.fn(),
}));
jest.mock('api/v2/sessions/rotate/post', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('AppRoutes/utils', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('../utils', () => ({
Logout: jest.fn(),
}));
// oxlint-disable-next-line typescript/no-require-imports typescript/no-var-requires
const post = require('api/v2/sessions/rotate/post').default;
// oxlint-disable-next-line typescript/no-require-imports typescript/no-var-requires
const { Logout } = require('../utils');
describe('interceptorRejected — no-auth mode', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(axios, 'isAxiosError').mockReturnValue(true);
});
it('does NOT call rotate or Logout when no-auth mode is enabled on 401', async () => {
(getIsNoAuthMode as jest.Mock).mockReturnValue(true);
const error = {
isAxiosError: true,
response: {
status: 401,
config: { url: '/dashboards', method: 'get' },
},
config: { url: '/dashboards', headers: {} },
};
await interceptorRejected(error as any).catch(() => {});
expect(post).not.toHaveBeenCalled();
expect(Logout).not.toHaveBeenCalled();
});
it('DOES attempt rotate when no-auth mode is disabled on 401', async () => {
(getIsNoAuthMode as jest.Mock).mockReturnValue(false);
(post as jest.Mock).mockResolvedValue({
data: { accessToken: 'a', refreshToken: 'b' },
});
const error = {
isAxiosError: true,
response: {
status: 401,
config: { url: '/dashboards', method: 'get' },
},
config: { url: '/dashboards', headers: {} },
};
await interceptorRejected(error as any).catch(() => {});
expect(post).toHaveBeenCalled();
});
});

View File

@@ -12,14 +12,10 @@ import type {
} from 'react-query';
import type {
InframonitoringtypesPostableClustersDTO,
InframonitoringtypesPostableHostsDTO,
InframonitoringtypesPostableNamespacesDTO,
InframonitoringtypesPostableNodesDTO,
InframonitoringtypesPostablePodsDTO,
ListClusters200,
ListHosts200,
ListNamespaces200,
ListNodes200,
ListPods200,
RenderErrorResponseDTO,
@@ -28,90 +24,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* @summary List Clusters for Infra Monitoring
*/
export const listClusters = (
inframonitoringtypesPostableClustersDTO: BodyType<InframonitoringtypesPostableClustersDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListClusters200>({
url: `/api/v2/infra_monitoring/clusters`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: inframonitoringtypesPostableClustersDTO,
signal,
});
};
export const getListClustersMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listClusters>>,
TError,
{ data: BodyType<InframonitoringtypesPostableClustersDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof listClusters>>,
TError,
{ data: BodyType<InframonitoringtypesPostableClustersDTO> },
TContext
> => {
const mutationKey = ['listClusters'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof listClusters>>,
{ data: BodyType<InframonitoringtypesPostableClustersDTO> }
> = (props) => {
const { data } = props ?? {};
return listClusters(data);
};
return { mutationFn, ...mutationOptions };
};
export type ListClustersMutationResult = NonNullable<
Awaited<ReturnType<typeof listClusters>>
>;
export type ListClustersMutationBody =
BodyType<InframonitoringtypesPostableClustersDTO>;
export type ListClustersMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List Clusters for Infra Monitoring
*/
export const useListClusters = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listClusters>>,
TError,
{ data: BodyType<InframonitoringtypesPostableClustersDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof listClusters>>,
TError,
{ data: BodyType<InframonitoringtypesPostableClustersDTO> },
TContext
> => {
const mutationOptions = getListClustersMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Returns a paginated list of hosts with key infrastructure metrics: CPU usage (%), memory usage (%), I/O wait (%), disk usage (%), and 15-minute load average. Each host includes its current status (active/inactive based on metrics reported in the last 10 minutes) and metadata attributes (e.g., os.type). Supports filtering via a filter expression, filtering by host status, custom groupBy to aggregate hosts by any attribute, ordering by any of the five metrics, and pagination via offset/limit. The response type is 'list' for the default host.name grouping or 'grouped_list' for custom groupBy keys. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (cpu, memory, wait, load15, diskUsage) return -1 as a sentinel when no data is available for that field.
* @summary List Hosts for Infra Monitoring
@@ -196,90 +108,6 @@ export const useListHosts = <
return useMutation(mutationOptions);
};
/**
* Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.
* @summary List Namespaces for Infra Monitoring
*/
export const listNamespaces = (
inframonitoringtypesPostableNamespacesDTO: BodyType<InframonitoringtypesPostableNamespacesDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListNamespaces200>({
url: `/api/v2/infra_monitoring/namespaces`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: inframonitoringtypesPostableNamespacesDTO,
signal,
});
};
export const getListNamespacesMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listNamespaces>>,
TError,
{ data: BodyType<InframonitoringtypesPostableNamespacesDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof listNamespaces>>,
TError,
{ data: BodyType<InframonitoringtypesPostableNamespacesDTO> },
TContext
> => {
const mutationKey = ['listNamespaces'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof listNamespaces>>,
{ data: BodyType<InframonitoringtypesPostableNamespacesDTO> }
> = (props) => {
const { data } = props ?? {};
return listNamespaces(data);
};
return { mutationFn, ...mutationOptions };
};
export type ListNamespacesMutationResult = NonNullable<
Awaited<ReturnType<typeof listNamespaces>>
>;
export type ListNamespacesMutationBody =
BodyType<InframonitoringtypesPostableNamespacesDTO>;
export type ListNamespacesMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List Namespaces for Infra Monitoring
*/
export const useListNamespaces = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listNamespaces>>,
TError,
{ data: BodyType<InframonitoringtypesPostableNamespacesDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof listNamespaces>>,
TError,
{ data: BodyType<InframonitoringtypesPostableNamespacesDTO> },
TContext
> => {
const mutationOptions = getListNamespacesMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } for pods scheduled on the listed nodes). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* @summary List Nodes for Infra Monitoring

View File

@@ -1839,7 +1839,6 @@ export enum AuthtypesRelationDTO {
delete = 'delete',
list = 'list',
assignee = 'assignee',
attach = 'attach',
}
export interface AuthtypesRoleDTO {
/**
@@ -4568,66 +4567,6 @@ export interface GlobaltypesTokenizerConfigDTO {
enabled?: boolean;
}
/**
* @nullable
*/
export type InframonitoringtypesClusterRecordDTOMeta = {
[key: string]: string;
} | null;
export interface InframonitoringtypesClusterRecordDTO {
/**
* @type number
* @format double
*/
clusterCPU: number;
/**
* @type number
* @format double
*/
clusterCPUAllocatable: number;
/**
* @type number
* @format double
*/
clusterMemory: number;
/**
* @type number
* @format double
*/
clusterMemoryAllocatable: number;
/**
* @type string
*/
clusterName: string;
/**
* @type object
* @nullable true
*/
meta: InframonitoringtypesClusterRecordDTOMeta;
nodeCountsByReadiness: InframonitoringtypesNodeCountsByReadinessDTO;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
}
export interface InframonitoringtypesClustersDTO {
/**
* @type boolean
*/
endTimeBeforeRetention: boolean;
/**
* @type array
* @nullable true
*/
records: InframonitoringtypesClusterRecordDTO[] | null;
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
total: number;
type: InframonitoringtypesResponseTypeDTO;
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesHostFilterDTO {
/**
* @type string
@@ -4713,55 +4652,6 @@ export interface InframonitoringtypesHostsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
/**
* @nullable
*/
export type InframonitoringtypesNamespaceRecordDTOMeta = {
[key: string]: string;
} | null;
export interface InframonitoringtypesNamespaceRecordDTO {
/**
* @type object
* @nullable true
*/
meta: InframonitoringtypesNamespaceRecordDTOMeta;
/**
* @type number
* @format double
*/
namespaceCPU: number;
/**
* @type number
* @format double
*/
namespaceMemory: number;
/**
* @type string
*/
namespaceName: string;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
}
export interface InframonitoringtypesNamespacesDTO {
/**
* @type boolean
*/
endTimeBeforeRetention: boolean;
/**
* @type array
* @nullable true
*/
records: InframonitoringtypesNamespaceRecordDTO[] | null;
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
total: number;
type: InframonitoringtypesResponseTypeDTO;
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export enum InframonitoringtypesNodeConditionDTO {
ready = 'ready',
not_ready = 'not_ready',
@@ -4945,34 +4835,6 @@ export interface InframonitoringtypesPodsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesPostableClustersDTO {
/**
* @type integer
* @format int64
*/
end: number;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array
* @nullable true
*/
groupBy?: Querybuildertypesv5GroupByKeyDTO[] | null;
/**
* @type integer
*/
limit: number;
/**
* @type integer
*/
offset?: number;
orderBy?: Querybuildertypesv5OrderByDTO;
/**
* @type integer
* @format int64
*/
start: number;
}
export interface InframonitoringtypesPostableHostsDTO {
/**
* @type integer
@@ -5001,34 +4863,6 @@ export interface InframonitoringtypesPostableHostsDTO {
start: number;
}
export interface InframonitoringtypesPostableNamespacesDTO {
/**
* @type integer
* @format int64
*/
end: number;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array
* @nullable true
*/
groupBy?: Querybuildertypesv5GroupByKeyDTO[] | null;
/**
* @type integer
*/
limit: number;
/**
* @type integer
*/
offset?: number;
orderBy?: Querybuildertypesv5OrderByDTO;
/**
* @type integer
* @format int64
*/
start: number;
}
export interface InframonitoringtypesPostableNodesDTO {
/**
* @type integer
@@ -9412,14 +9246,6 @@ export type Healthz503 = {
status: string;
};
export type ListClusters200 = {
data: InframonitoringtypesClustersDTO;
/**
* @type string
*/
status: string;
};
export type ListHosts200 = {
data: InframonitoringtypesHostsDTO;
/**
@@ -9428,14 +9254,6 @@ export type ListHosts200 = {
status: string;
};
export type ListNamespaces200 = {
data: InframonitoringtypesNamespacesDTO;
/**
* @type string
*/
status: string;
};
export type ListNodes200 = {
data: InframonitoringtypesNodesDTO;
/**

View File

@@ -13,6 +13,7 @@ import { Events } from 'constants/events';
import { LOCALSTORAGE } from 'constants/localStorage';
import { getBasePath } from 'utils/basePath';
import { eventEmitter } from 'utils/getEventEmitter';
import { getIsNoAuthMode } from 'utils/noAuthMode';
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4, apiV5 } from './apiV1';
import { Logout } from './utils';
@@ -108,7 +109,10 @@ export const interceptorRejected = async (
if (axios.isAxiosError(value) && value.response) {
const { response } = value;
const isNoAuthMode = getIsNoAuthMode();
if (
!isNoAuthMode &&
response.status === 401 &&
// if the session rotate call or the create session errors out with 401 or the delete sessions call returns 401 then we do not retry!
response.config.url !== '/sessions/rotate' &&
@@ -140,16 +144,20 @@ export const interceptorRejected = async (
return await Promise.resolve(reResponse);
} catch (error) {
if ((error as AxiosError)?.response?.status === 401) {
Logout();
void Logout();
}
}
} catch (error) {
Logout();
void Logout();
}
}
if (response.status === 401 && response.config.url === '/sessions/rotate') {
Logout();
if (
!isNoAuthMode &&
response.status === 401 &&
response.config.url === '/sessions/rotate'
) {
void Logout();
}
}
return await Promise.reject(value);

View File

@@ -1,4 +1,4 @@
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import get from 'api/browser/localstorage/get';
import { LOCALSTORAGE } from 'constants/localStorage';
import { THEME_MODE } from 'hooks/useDarkMode/constant';

View File

@@ -1,5 +1,5 @@
import { useCallback } from 'react';
import { Button } from '@signozhq/ui/button';
import { Button } from '@signozhq/ui';
import { LifeBuoy } from 'lucide-react';
import signozBrandLogoUrl from '@/assets/Logos/signoz-brand-logo.svg';

View File

@@ -2,9 +2,8 @@ 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 { SearchOutlined } from '@ant-design/icons';
import { LoadingOutlined, SearchOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Loader } from '@signozhq/icons';
import {
Button,
Input,
@@ -16,7 +15,7 @@ import {
TableColumnType,
Tooltip,
} from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import type { FilterDropdownProps } from 'antd/lib/table/interface';
import logEvent from 'api/common/logEvent';
import {
@@ -517,9 +516,7 @@ export default function CeleryOverviewTable({
bordered={false}
loading={{
spinning: isLoading,
indicator: (
<Spin indicator={<Loader size={14} className="animate-spin" />} />
),
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: isLoading ? null : <Typography.Text>No data</Typography.Text>,

View File

@@ -1,6 +1,6 @@
import { useHistory, useLocation } from 'react-router-dom';
import { Select, Spin } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import { SelectMaxTagPlaceholder } from 'components/MessagingQueues/MQCommon/MQCommon';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Divider, Drawer } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import logEvent from 'api/common/logEvent';
import { PANEL_TYPES } from 'constants/queryBuilder';
import dayjs from 'dayjs';

View File

@@ -2,7 +2,7 @@ import { useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Card } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import logEvent from 'api/common/logEvent';
import { CardContainer } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { Button, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import logEvent from 'api/common/logEvent';
import updateCreditCardApi from 'api/v1/checkout/create';
import { useNotifications } from 'hooks/useNotifications';

View File

@@ -1,7 +1,7 @@
import { useMemo, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Check, Copy } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Button } from '@signozhq/ui';
import SyntaxHighlighter, {
a11yDark,
} from 'components/MarkdownRenderer/syntaxHighlighter';

View File

@@ -1,10 +1,13 @@
import { Controller, useForm } from 'react-hook-form';
import { useQueryClient } from 'react-query';
import { X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DialogFooter, DialogWrapper } from '@signozhq/ui/dialog';
import { Input } from '@signozhq/ui/input';
import { toast } from '@signozhq/ui/sonner';
import {
Button,
DialogFooter,
DialogWrapper,
Input,
toast,
} from '@signozhq/ui';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
invalidateListServiceAccounts,

View File

@@ -1,4 +1,4 @@
import { toast } from '@signozhq/ui/sonner';
import { toast } from '@signozhq/ui';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import {
@@ -11,8 +11,8 @@ import {
import CreateServiceAccountModal from '../CreateServiceAccountModal';
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: { success: jest.fn(), error: jest.fn() },
}));

View File

@@ -1,4 +1,4 @@
import { Calendar } from '@signozhq/ui/calendar';
import { Calendar } from '@signozhq/ui';
import { Button } from 'antd';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import dayjs from 'dayjs';

View File

@@ -7,7 +7,7 @@ import {
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { Button } from '@signozhq/ui';
import { Input, InputRef, Popover, Tooltip } from 'antd';
import cx from 'classnames';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';

View File

@@ -1,5 +1,5 @@
import { ReactNode } from 'react';
import { Button } from '@signozhq/ui/button';
import { Button } from '@signozhq/ui';
import { X } from '@signozhq/icons';
import './DetailsHeader.styles.scss';

View File

@@ -1,4 +1,4 @@
import { DrawerWrapper } from '@signozhq/ui/drawer';
import { DrawerWrapper } from '@signozhq/ui';
import './DetailsPanelDrawer.styles.scss';

View File

@@ -1,6 +1,6 @@
import { useCallback, useMemo, useState } from 'react';
import { Button, Popover, Radio, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import { TelemetryFieldKey } from 'api/v5/v5';
import { useExportRawData } from 'hooks/useDownloadOptionsMenu/useDownloadOptionsMenu';
import { Download, DownloadIcon, Loader2 } from 'lucide-react';

View File

@@ -1,6 +1,5 @@
import { Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { Button, DialogWrapper } from '@signozhq/ui';
import { MemberRow } from 'components/MembersTable/MembersTable';
interface DeleteMemberDialogProps {

View File

@@ -1,11 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { LockKeyhole, RefreshCw, Trash2, X } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { DrawerWrapper } from '@signozhq/ui/drawer';
import { Input } from '@signozhq/ui/input';
import { toast } from '@signozhq/ui/sonner';
import { Badge, Button, DrawerWrapper, Input, toast } from '@signozhq/ui';
import { Skeleton, Tooltip } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
@@ -19,6 +15,7 @@ import {
} from 'api/generated/services/users';
import { AxiosError } from 'axios';
import { MemberRow } from 'components/MembersTable/MembersTable';
import { NoAuthGuard } from 'components/NoAuthGuard';
import RolesSelect, { useRoles } from 'components/RolesSelect';
import SaveErrorItem from 'components/ServiceAccountDrawer/SaveErrorItem';
import type { SaveError } from 'components/ServiceAccountDrawer/utils';
@@ -596,39 +593,43 @@ function EditMemberDrawer({
<div className="edit-member-drawer__footer-left">
<Tooltip title={getDeleteTooltip(isRootUser, isSelf)}>
<span className="edit-member-drawer__tooltip-wrapper">
<Button
onClick={(): void => setShowDeleteConfirm(true)}
disabled={isRootUser || isSelf}
variant="link"
color="destructive"
>
<Trash2 size={12} />
{isInvited ? 'Revoke Invite' : 'Delete Member'}
</Button>
<NoAuthGuard>
<Button
onClick={(): void => setShowDeleteConfirm(true)}
disabled={isRootUser || isSelf}
variant="link"
color="destructive"
>
<Trash2 size={12} />
{isInvited ? 'Revoke Invite' : 'Delete Member'}
</Button>
</NoAuthGuard>
</span>
</Tooltip>
<div className="edit-member-drawer__footer-divider" />
<Tooltip title={isRootUser ? ROOT_USER_TOOLTIP : undefined}>
<span className="edit-member-drawer__tooltip-wrapper">
<Button
onClick={handleGenerateResetLink}
disabled={isGeneratingLink || isRootUser || isLoadingTokenStatus}
variant="link"
color="warning"
>
<RefreshCw size={12} />
{isGeneratingLink
? 'Generating...'
: isInvited
? getInviteButtonLabel(
isLoadingTokenStatus,
existingToken,
isTokenExpired,
tokenNotFound,
)
: 'Generate Password Reset Link'}
</Button>
<NoAuthGuard>
<Button
onClick={handleGenerateResetLink}
disabled={isGeneratingLink || isRootUser || isLoadingTokenStatus}
variant="link"
color="warning"
>
<RefreshCw size={12} />
{isGeneratingLink
? 'Generating...'
: isInvited
? getInviteButtonLabel(
isLoadingTokenStatus,
existingToken,
isTokenExpired,
tokenNotFound,
)
: 'Generate Password Reset Link'}
</Button>
</NoAuthGuard>
</span>
</Tooltip>
</div>
@@ -639,15 +640,17 @@ function EditMemberDrawer({
Cancel
</Button>
<Button
variant="solid"
color="primary"
disabled={!isDirty || isSaving || isRootUser}
onClick={handleSave}
loading={isSaving}
>
{isSaving ? 'Saving...' : 'Save Member Details'}
</Button>
<NoAuthGuard>
<Button
variant="solid"
color="primary"
disabled={!isDirty || isSaving || isRootUser}
onClick={handleSave}
loading={isSaving}
>
{isSaving ? 'Saving...' : 'Save Member Details'}
</Button>
</NoAuthGuard>
</div>
</>
)}

View File

@@ -1,6 +1,5 @@
import { Check, Copy } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { Button, DialogWrapper } from '@signozhq/ui';
interface ResetLinkDialogProps {
open: boolean;

View File

@@ -1,5 +1,5 @@
import type { ReactNode } from 'react';
import { toast } from '@signozhq/ui/sonner';
import { toast } from '@signozhq/ui';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
useCreateResetPasswordToken,
@@ -34,8 +34,8 @@ jest.mock('api/ErrorResponseHandlerForGeneratedAPIs', () => ({
convertToApiError: jest.fn(),
}));
jest.mock('@signozhq/ui/drawer', () => ({
...jest.requireActual('@signozhq/ui/drawer'),
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
DrawerWrapper: ({
children,
footer,
@@ -51,10 +51,6 @@ jest.mock('@signozhq/ui/drawer', () => ({
{footer}
</div>
) : null,
}));
jest.mock('@signozhq/ui/dialog', () => ({
...jest.requireActual('@signozhq/ui/dialog'),
DialogWrapper: ({
children,
footer,
@@ -75,10 +71,6 @@ jest.mock('@signozhq/ui/dialog', () => ({
DialogFooter: ({ children }: { children?: ReactNode }): JSX.Element => (
<div>{children}</div>
),
}));
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: {
success: jest.fn(),
error: jest.fn(),

View File

@@ -16,7 +16,7 @@ import {
Select,
Space,
} from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import axios from 'axios';
import TextToolTip from 'components/TextToolTip';
import { SOMETHING_WENT_WRONG } from 'constants/api';

View File

@@ -1,7 +1,7 @@
import { MouseEvent, useCallback } from 'react';
import { DeleteOutlined } from '@ant-design/icons';
import { Col, Row, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';

View File

@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Card, Form, Input } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSaveView } from 'hooks/saveViews/useSaveView';

View File

@@ -1,4 +1,4 @@
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
function AnnouncementsModal(): JSX.Element {
return (

View File

@@ -1,8 +1,8 @@
import { useCallback, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { toast } from '@signozhq/ui/sonner';
import { toast } from '@signozhq/ui';
import { Button, Input, Radio, RadioChangeEvent } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import logEvent from 'api/common/logEvent';
import { handleContactSupport } from 'container/Integrations/utils';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';

View File

@@ -5,7 +5,7 @@ import { matchPath, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import { Button, Switch } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';

View File

@@ -1,6 +1,6 @@
// Mock dependencies before imports
import { useLocation } from 'react-router-dom';
import { toast } from '@signozhq/ui/sonner';
import { toast } from '@signozhq/ui';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import logEvent from 'api/common/logEvent';
@@ -19,8 +19,8 @@ jest.mock('react-router-dom', () => ({
useLocation: jest.fn(),
}));
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: {
success: jest.fn(),
error: jest.fn(),

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import logEvent from 'api/common/logEvent';
import { useNotifications } from 'hooks/useNotifications';
import { CheckCircle2, HandPlatter } from 'lucide-react';

View File

@@ -1,4 +1,4 @@
import { Badge } from '@signozhq/ui/badge';
import { Badge } from '@signozhq/ui';
type BadgeColor =
| 'vanilla'

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { Button, Input } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import cx from 'classnames';
import { X } from 'lucide-react';

View File

@@ -1,11 +1,14 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Style } from '@signozhq/design-tokens';
import { ChevronDown, Plus, Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Callout } from '@signozhq/ui/callout';
import { DialogFooter, DialogWrapper } from '@signozhq/ui/dialog';
import { Input } from '@signozhq/ui/input';
import { toast } from '@signozhq/ui/sonner';
import {
Button,
Callout,
DialogFooter,
DialogWrapper,
Input,
toast,
} from '@signozhq/ui';
import { Select } from 'antd';
import inviteUsers from 'api/v1/invite/bulk/create';
import sendInvite from 'api/v1/invite/create';

View File

@@ -14,8 +14,8 @@ const makeApiError = (message: string, code = StatusCodes.CONFLICT): APIError =>
jest.mock('api/v1/invite/create');
jest.mock('api/v1/invite/bulk/create');
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: {
success: jest.fn(),
error: jest.fn(),

View File

@@ -2,7 +2,7 @@ import { useMemo, useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { Button, Modal, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import logEvent from 'api/common/logEvent';
import updateCreditCardApi from 'api/v1/checkout/create';
import cx from 'classnames';

View File

@@ -3,9 +3,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useCopyToClipboard, useLocation } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { Button } from '@signozhq/ui';
import { Divider, Drawer, Radio, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import type { RadioChangeEvent } from 'antd/lib';
import cx from 'classnames';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';

View File

@@ -1,4 +1,4 @@
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import styled from 'styled-components';
export const CategoryHeadingText = styled(Typography.Text)`

View File

@@ -1,6 +1,6 @@
import { memo, useCallback, useMemo } from 'react';
import { blue } from '@ant-design/colors';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import cx from 'classnames';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Input, InputNumber, Popover, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import type { DefaultOptionType } from 'antd/es/select';
import cx from 'classnames';
import { LogViewMode } from 'container/LogsTable';

View File

@@ -1,5 +1,5 @@
import type React from 'react';
import { Badge } from '@signozhq/ui/badge';
import { Badge } from '@signozhq/ui';
import { Table, Tooltip } from 'antd';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';

View File

@@ -1,8 +1,7 @@
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import { ReactNode, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { CaretDownOutlined } from '@ant-design/icons';
import { Loader } from '@signozhq/icons';
import { CaretDownOutlined, LoadingOutlined } from '@ant-design/icons';
import { Modal, Select, Spin, Tooltip, Tree, TreeDataNode } from 'antd';
import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnboardingStatus';
import { QueryParams } from 'constants/query';
@@ -223,7 +222,7 @@ function AttributeCheckList({
>
{loading ? (
<div className="loader-container">
<Spin indicator={<Loader className="animate-spin" />} size="large" />
<Spin indicator={<LoadingOutlined spin />} size="large" />
</div>
) : (
<div className="modal-content">

View File

@@ -7,11 +7,14 @@ import React, {
useState,
} from 'react';
import { Virtuoso } from 'react-virtuoso';
import { DownOutlined, ReloadOutlined } from '@ant-design/icons';
import {
DownOutlined,
LoadingOutlined,
ReloadOutlined,
} from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Loader } from '@signozhq/icons';
import { Button, Checkbox, Select } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import cx from 'classnames';
import TextToolTip from 'components/TextToolTip/TextToolTip';
import { SOMETHING_WENT_WRONG } from 'constants/api';
@@ -1697,7 +1700,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
{loading && (
<div className="navigation-loading">
<div className="navigation-icons">
<Loader className="animate-spin" />
<LoadingOutlined />
</div>
<div className="navigation-text">Refreshing values...</div>
</div>
@@ -1705,7 +1708,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
{!loading && waitingMessage && (
<div className="navigation-loading">
<div className="navigation-icons">
<Loader className="animate-spin" />
<LoadingOutlined />
</div>
<div className="navigation-text" title={waitingMessage}>
{waitingMessage}

View File

@@ -6,9 +6,13 @@ import React, {
useRef,
useState,
} from 'react';
import { CloseOutlined, DownOutlined, ReloadOutlined } from '@ant-design/icons';
import {
CloseOutlined,
DownOutlined,
LoadingOutlined,
ReloadOutlined,
} from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Loader } from '@signozhq/icons';
import { Select } from 'antd';
import cx from 'classnames';
import TextToolTip from 'components/TextToolTip';
@@ -577,7 +581,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
{loading && (
<div className="navigation-loading">
<div className="navigation-icons">
<Loader className="animate-spin" />
<LoadingOutlined />
</div>
<div className="navigation-text">Refreshing values...</div>
</div>
@@ -585,7 +589,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
{!loading && waitingMessage && (
<div className="navigation-loading">
<div className="navigation-icons">
<Loader className="animate-spin" />
<LoadingOutlined />
</div>
<div className="navigation-text" title={waitingMessage}>
{waitingMessage}

View File

@@ -0,0 +1,3 @@
.banner {
height: var(--spacing-20);
}

View File

@@ -0,0 +1,18 @@
import { PersistedAnnouncementBanner } from '@signozhq/ui';
import styles from './NoAuthBanner.module.scss';
export function NoAuthBanner(): JSX.Element {
return (
<PersistedAnnouncementBanner
type="warning"
storageKey="no-auth-banner-v1"
testId="no-auth-banner"
className={styles.banner}
>
No-auth mode: authentication is disabled, network is the trust boundary.
</PersistedAnnouncementBanner>
);
}
export default NoAuthBanner;

View File

@@ -0,0 +1,17 @@
import { render, screen } from 'tests/test-utils';
import { NoAuthBanner } from '../NoAuthBanner';
describe('NoAuthBanner', () => {
it('renders the no-auth message', () => {
render(<NoAuthBanner />);
expect(
screen.getByText(/No-auth mode: authentication is disabled/i),
).toBeInTheDocument();
});
it('renders with the warning test id', () => {
render(<NoAuthBanner />);
expect(screen.getByTestId('no-auth-banner')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { Tooltip, TooltipProvider } from '@signozhq/ui';
import { useAppContext } from 'providers/App/App';
export const DEFAULT_MESSAGE = 'Not available in no-auth mode';
interface NoAuthGuardProps {
children: React.ReactElement;
message?: string;
}
export function NoAuthGuard({
children,
message = DEFAULT_MESSAGE,
}: NoAuthGuardProps): JSX.Element {
const { isNoAuthMode } = useAppContext();
if (!isNoAuthMode) {
return children;
}
const disabledChild = React.cloneElement(children, { disabled: true });
return (
<TooltipProvider>
<Tooltip title={message} arrow>
<span
data-no-auth-trigger
style={{ display: 'inline-flex', cursor: 'not-allowed' }}
>
{disabledChild}
</span>
</Tooltip>
</TooltipProvider>
);
}

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { render } from 'tests/test-utils';
import { DEFAULT_MESSAGE, NoAuthGuard } from '..';
describe('NoAuthGuard', () => {
it('renders children unchanged when isNoAuthMode is false', () => {
const { getByRole } = render(
<NoAuthGuard>
<button type="button">Action</button>
</NoAuthGuard>,
undefined,
{ appContextOverrides: { isNoAuthMode: false } },
);
expect(getByRole('button', { name: 'Action' })).not.toBeDisabled();
});
it('does not intercept onClick when isNoAuthMode is false', () => {
const handleClick = jest.fn();
const { getByRole } = render(
<NoAuthGuard>
<button type="button" onClick={handleClick}>
Action
</button>
</NoAuthGuard>,
undefined,
{ appContextOverrides: { isNoAuthMode: false } },
);
getByRole('button', { name: 'Action' }).click();
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('disables children when isNoAuthMode is true', () => {
const { getByRole } = render(
<NoAuthGuard>
<button type="button">Action</button>
</NoAuthGuard>,
undefined,
{ appContextOverrides: { isNoAuthMode: true } },
);
expect(getByRole('button', { name: 'Action' })).toBeDisabled();
});
it('renders a tooltip trigger wrapper when isNoAuthMode is true', () => {
const { container } = render(
<NoAuthGuard>
<button type="button">Action</button>
</NoAuthGuard>,
undefined,
{ appContextOverrides: { isNoAuthMode: true } },
);
expect(
container.querySelector('span[data-no-auth-trigger]'),
).toBeInTheDocument();
});
it('overrides existing disabled prop — no-auth always wins', () => {
const { getByRole } = render(
// eslint-disable-next-line react/button-has-type
<NoAuthGuard>
<button type="button" disabled={false}>
Action
</button>
</NoAuthGuard>,
undefined,
{ appContextOverrides: { isNoAuthMode: true } },
);
expect(getByRole('button', { name: 'Action' })).toBeDisabled();
});
it('exports DEFAULT_MESSAGE as a non-empty string', () => {
expect(typeof DEFAULT_MESSAGE).toBe('string');
expect(DEFAULT_MESSAGE.length).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1 @@
export { DEFAULT_MESSAGE, NoAuthGuard } from './NoAuthGuard';

View File

@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { Button, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import WarningPopover from 'components/WarningPopover/WarningPopover';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';

View File

@@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { Button, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import cx from 'classnames';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';

View File

@@ -1,4 +1,4 @@
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import eyesEmojiUrl from 'assets/Images/eyesEmoji.svg';
import styles from './QueryCancelledPlaceholder.module.scss';

View File

@@ -1,7 +1,7 @@
/* eslint-disable sonarjs/no-identical-functions */
import { Fragment, useMemo, useState } from 'react';
import { Button, Checkbox, Input, Skeleton } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import cx from 'classnames';
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
import {

View File

@@ -11,9 +11,9 @@ import {
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@signozhq/ui/combobox';
} from '@signozhq/ui';
import { Skeleton, Switch, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { Button } from '@signozhq/ui/button';
import { Button } from '@signozhq/ui';
import classNames from 'classnames';
import { Check, X } from 'lucide-react';

View File

@@ -1,4 +1,4 @@
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import Time from './Time';

View File

@@ -1,4 +1,4 @@
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { useTimezone } from 'providers/Timezone';

View File

@@ -1,7 +1,5 @@
import { Check, Copy } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Callout } from '@signozhq/ui/callout';
import { Badge, Button, Callout } from '@signozhq/ui';
import type { ServiceaccounttypesGettableFactorAPIKeyWithKeyDTO } from 'api/generated/services/sigNoz.schemas';
export interface KeyCreatedPhaseProps {

View File

@@ -1,8 +1,6 @@
import type { Control, UseFormRegister } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import { Button, Input, ToggleGroup, ToggleGroupItem } from '@signozhq/ui';
import { DatePicker } from 'antd';
import { popupContainer } from 'utils/selectPopupContainer';

View File

@@ -2,8 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useQueryClient } from 'react-query';
import { useCopyToClipboard } from 'react-use';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { toast } from '@signozhq/ui/sonner';
import { DialogWrapper, toast } from '@signozhq/ui';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
invalidateListServiceAccountKeys,

View File

@@ -1,8 +1,6 @@
import { useQueryClient } from 'react-query';
import { Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { toast } from '@signozhq/ui/sonner';
import { Button, DialogWrapper, toast } from '@signozhq/ui';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
getGetServiceAccountQueryKey,

View File

@@ -1,10 +1,13 @@
import type { Control, UseFormRegister } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import { LockKeyhole, Trash2, X } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import {
Badge,
Button,
Input,
ToggleGroup,
ToggleGroupItem,
} from '@signozhq/ui';
import { DatePicker } from 'antd';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { popupContainer } from 'utils/selectPopupContainer';

View File

@@ -1,8 +1,7 @@
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useQueryClient } from 'react-query';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { toast } from '@signozhq/ui/sonner';
import { DialogWrapper, toast } from '@signozhq/ui';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
invalidateListServiceAccountKeys,

View File

@@ -1,7 +1,9 @@
import { useCallback, useMemo } from 'react';
import { KeyRound, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Button } from '@signozhq/ui';
import { Skeleton, Table, Tooltip } from 'antd';
import { DEFAULT_MESSAGE, NoAuthGuard } from 'components/NoAuthGuard';
import { useAppContext } from 'providers/App/App';
import type { ColumnsType } from 'antd/es/table/interface';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
@@ -23,6 +25,7 @@ interface KeysTabProps {
interface BuildColumnsParams {
isDisabled: boolean;
isNoAuthMode: boolean;
onRevokeClick: (keyId: string) => void;
handleformatLastObservedAt: (
lastObservedAt: Date | null | undefined,
@@ -42,6 +45,7 @@ function formatExpiry(expiresAt: number): JSX.Element {
function buildColumns({
isDisabled,
isNoAuthMode,
onRevokeClick,
handleformatLastObservedAt,
}: BuildColumnsParams): ColumnsType<ServiceaccounttypesGettableFactorAPIKeyDTO> {
@@ -92,23 +96,30 @@ function buildColumns({
key: 'action',
width: 48,
align: 'right' as const,
render: (_, record): JSX.Element => (
<Tooltip title={isDisabled ? 'Service account disabled' : 'Revoke Key'}>
<Button
variant="ghost"
size="sm"
color="destructive"
disabled={isDisabled}
onClick={(e): void => {
e.stopPropagation();
onRevokeClick(record.id);
}}
className="keys-tab__revoke-btn"
>
<X size={12} />
</Button>
</Tooltip>
),
render: (_, record): JSX.Element => {
const tooltipTitle = isDisabled
? 'Service account disabled'
: isNoAuthMode
? DEFAULT_MESSAGE
: 'Revoke Key';
return (
<Tooltip title={tooltipTitle}>
<Button
variant="ghost"
size="sm"
color="destructive"
disabled={isDisabled || isNoAuthMode}
onClick={(e): void => {
e.stopPropagation();
onRevokeClick(record.id);
}}
className="keys-tab__revoke-btn"
>
<X size={12} />
</Button>
</Tooltip>
);
},
},
];
}
@@ -134,6 +145,7 @@ function KeysTab({
parseAsString.withDefault(''),
);
const editKey = keys.find((k) => k.id === editKeyId) ?? null;
const { isNoAuthMode } = useAppContext();
const handleformatLastObservedAt = useCallback(
(lastObservedAt: Date | null | undefined): string =>
@@ -143,14 +155,20 @@ function KeysTab({
const onRevokeClick = useCallback(
(keyId: string): void => {
setRevokeKeyId(keyId);
void setRevokeKeyId(keyId);
},
[setRevokeKeyId],
);
const columns = useMemo(
() => buildColumns({ isDisabled, onRevokeClick, handleformatLastObservedAt }),
[isDisabled, onRevokeClick, handleformatLastObservedAt],
() =>
buildColumns({
isDisabled,
isNoAuthMode,
onRevokeClick,
handleformatLastObservedAt,
}),
[isDisabled, isNoAuthMode, onRevokeClick, handleformatLastObservedAt],
);
if (isLoading) {
@@ -176,16 +194,18 @@ function KeysTab({
Learn more
</a>
</p>
<Button
variant="link"
color="primary"
onClick={async (): Promise<void> => {
await setIsAddKeyOpen(true);
}}
disabled={isDisabled}
>
+ Add your first key
</Button>
<NoAuthGuard>
<Button
variant="link"
color="primary"
onClick={async (): Promise<void> => {
await setIsAddKeyOpen(true);
}}
disabled={isDisabled}
>
+ Add your first key
</Button>
</NoAuthGuard>
</div>
);
}

View File

@@ -1,7 +1,6 @@
import { useCallback } from 'react';
import { LockKeyhole } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Input } from '@signozhq/ui/input';
import { Badge, Input } from '@signozhq/ui';
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import RolesSelect from 'components/RolesSelect';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
@@ -16,8 +15,8 @@ interface OverviewTabProps {
account: ServiceAccountRow;
localName: string;
onNameChange: (v: string) => void;
localRoles: string[];
onRolesChange: (v: string[]) => void;
localRole: string;
onRoleChange: (v: string | undefined) => void;
isDisabled: boolean;
availableRoles: AuthtypesRoleDTO[];
rolesLoading?: boolean;
@@ -31,8 +30,8 @@ function OverviewTab({
account,
localName,
onNameChange,
localRoles,
onRolesChange,
localRole,
onRoleChange,
isDisabled,
availableRoles,
rolesLoading,
@@ -95,15 +94,10 @@ function OverviewTab({
{isDisabled ? (
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
<div className="sa-drawer__disabled-roles">
{localRoles.length > 0 ? (
localRoles.map((roleId) => {
const role = availableRoles.find((r) => r.id === roleId);
return (
<Badge key={roleId} color="vanilla">
{role?.name ?? roleId}
</Badge>
);
})
{localRole ? (
<Badge color="vanilla">
{availableRoles.find((r) => r.id === localRole)?.name ?? localRole}
</Badge>
) : (
<span className="sa-drawer__input-text"></span>
)}
@@ -113,15 +107,14 @@ function OverviewTab({
) : (
<RolesSelect
id="sa-roles"
mode="multiple"
roles={availableRoles}
loading={rolesLoading}
isError={rolesError}
error={rolesErrorObj}
onRefetch={onRefetchRoles}
value={localRoles}
onChange={onRolesChange}
placeholder="Select roles"
value={localRole}
onChange={onRoleChange}
placeholder="Select role"
/>
)}
</div>

View File

@@ -1,8 +1,6 @@
import { useQueryClient } from 'react-query';
import { Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { toast } from '@signozhq/ui/sonner';
import { Button, DialogWrapper, toast } from '@signozhq/ui';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
getListServiceAccountKeysQueryKey,

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import { Color } from '@signozhq/design-tokens';
import { ChevronDown, ChevronUp, CircleAlert, RotateCw } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Button } from '@signozhq/ui';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import APIError from 'types/api/error';

View File

@@ -1,14 +1,19 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQueryClient } from 'react-query';
import { Key, LayoutGrid, Plus, Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DrawerWrapper } from '@signozhq/ui/drawer';
import { toast } from '@signozhq/ui/sonner';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import {
Button,
DrawerWrapper,
toast,
ToggleGroup,
ToggleGroupItem,
} from '@signozhq/ui';
import { Pagination, Skeleton } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
getGetServiceAccountRolesQueryKey,
getListServiceAccountsQueryKey,
useDeleteServiceAccountRole,
useGetServiceAccount,
useListServiceAccountKeys,
useUpdateServiceAccount,
@@ -35,8 +40,9 @@ import {
useQueryState,
} from 'nuqs';
import APIError from 'types/api/error';
import { toAPIError } from 'utils/errorUtils';
import { retryOn429, toAPIError } from 'utils/errorUtils';
import { NoAuthGuard } from 'components/NoAuthGuard';
import AddKeyModal from './AddKeyModal';
import DeleteAccountModal from './DeleteAccountModal';
import KeysTab from './KeysTab';
@@ -90,7 +96,7 @@ function ServiceAccountDrawer({
parseAsBoolean.withDefault(false),
);
const [localName, setLocalName] = useState('');
const [localRoles, setLocalRoles] = useState<string[]>([]);
const [localRole, setLocalRole] = useState('');
const [isSaving, setIsSaving] = useState(false);
const [saveErrors, setSaveErrors] = useState<SaveError[]>([]);
@@ -138,7 +144,7 @@ function ServiceAccountDrawer({
if (!account?.id) {
roleSessionRef.current = null;
} else if (account.id !== roleSessionRef.current && !isRolesLoading) {
setLocalRoles(currentRoles.map((r) => r.id).filter(Boolean) as string[]);
setLocalRole(currentRoles[0]?.id ?? '');
roleSessionRef.current = account.id;
}
}, [account?.id, currentRoles, isRolesLoading]);
@@ -149,13 +155,7 @@ function ServiceAccountDrawer({
const isDirty =
account !== null &&
(localName !== (account.name ?? '') ||
JSON.stringify([...localRoles].sort()) !==
JSON.stringify(
currentRoles
.map((r) => r.id)
.filter(Boolean)
.sort(),
));
localRole !== (currentRoles[0]?.id ?? ''));
const {
roles: availableRoles,
@@ -183,6 +183,27 @@ function ServiceAccountDrawer({
// the retry for this mutation is safe due to the api being idempotent on backend
const { mutateAsync: updateMutateAsync } = useUpdateServiceAccount();
const { mutateAsync: deleteRole } = useDeleteServiceAccountRole({
mutation: {
retry: retryOn429,
},
});
const executeRolesOperation = useCallback(
async (accountId: string): Promise<RoleUpdateFailure[]> => {
if (localRole === '' && currentRoles[0]?.id) {
await deleteRole({
pathParams: { id: accountId, rid: currentRoles[0].id },
});
await queryClient.invalidateQueries(
getGetServiceAccountRolesQueryKey({ id: accountId }),
);
return [];
}
return applyDiff([localRole].filter(Boolean), availableRoles);
},
[localRole, currentRoles, availableRoles, applyDiff, deleteRole, queryClient],
);
const retryNameUpdate = useCallback(async (): Promise<void> => {
if (!account) {
@@ -250,7 +271,7 @@ function ServiceAccountDrawer({
const retryRolesUpdate = useCallback(async (): Promise<void> => {
try {
const failures = await applyDiff([...localRoles], availableRoles);
const failures = await executeRolesOperation(selectedAccountId ?? '');
if (failures.length === 0) {
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Roles update'));
} else {
@@ -266,7 +287,7 @@ function ServiceAccountDrawer({
),
);
}
}, [localRoles, availableRoles, applyDiff, failuresToSaveErrors]);
}, [selectedAccountId, executeRolesOperation, failuresToSaveErrors]);
const handleSave = useCallback(async (): Promise<void> => {
if (!account || !isDirty) {
@@ -285,7 +306,7 @@ function ServiceAccountDrawer({
const [nameResult, rolesResult] = await Promise.allSettled([
namePromise,
applyDiff([...localRoles], availableRoles),
executeRolesOperation(account.id),
]);
const errors: SaveError[] = [];
@@ -326,10 +347,8 @@ function ServiceAccountDrawer({
account,
isDirty,
localName,
localRoles,
availableRoles,
updateMutateAsync,
applyDiff,
executeRolesOperation,
refetchAccount,
onSuccess,
queryClient,
@@ -392,18 +411,20 @@ function ServiceAccountDrawer({
</ToggleGroupItem>
</ToggleGroup>
{activeTab === ServiceAccountDrawerTab.Keys && (
<Button
variant="outlined"
size="sm"
color="secondary"
disabled={isDeleted}
onClick={(): void => {
void setIsAddKeyOpen(true);
}}
>
<Plus size={12} />
Add Key
</Button>
<NoAuthGuard>
<Button
variant="outlined"
size="sm"
color="secondary"
disabled={isDeleted}
onClick={(): void => {
void setIsAddKeyOpen(true);
}}
>
<Plus size={12} />
Add Key
</Button>
</NoAuthGuard>
)}
</div>
@@ -428,9 +449,9 @@ function ServiceAccountDrawer({
account={account}
localName={localName}
onNameChange={handleNameChange}
localRoles={localRoles}
onRolesChange={(roles): void => {
setLocalRoles(roles);
localRole={localRole}
onRoleChange={(role): void => {
setLocalRole(role ?? '');
clearRoleErrors();
}}
isDisabled={isDeleted}
@@ -482,16 +503,18 @@ function ServiceAccountDrawer({
) : (
<>
{!isDeleted && (
<Button
variant="link"
color="destructive"
onClick={(): void => {
void setIsDeleteOpen(true);
}}
>
<Trash2 size={12} />
Delete Service Account
</Button>
<NoAuthGuard>
<Button
variant="link"
color="destructive"
onClick={(): void => {
void setIsDeleteOpen(true);
}}
>
<Trash2 size={12} />
Delete Service Account
</Button>
</NoAuthGuard>
)}
{!isDeleted && (
<div className="sa-drawer__footer-right">
@@ -499,15 +522,17 @@ function ServiceAccountDrawer({
<X size={14} />
Cancel
</Button>
<Button
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
onClick={handleSave}
>
Save Changes
</Button>
<NoAuthGuard>
<Button
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
onClick={handleSave}
>
Save Changes
</Button>
</NoAuthGuard>
</div>
)}
</>

View File

@@ -1,4 +1,4 @@
import { toast } from '@signozhq/ui/sonner';
import { toast } from '@signozhq/ui';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import {
@@ -11,8 +11,8 @@ import {
import AddKeyModal from '../AddKeyModal';
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: { success: jest.fn(), error: jest.fn() },
}));

View File

@@ -1,4 +1,4 @@
import { toast } from '@signozhq/ui/sonner';
import { toast } from '@signozhq/ui';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
@@ -6,8 +6,8 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import EditKeyModal from '../EditKeyModal';
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: { success: jest.fn(), error: jest.fn() },
}));

View File

@@ -1,4 +1,4 @@
import { toast } from '@signozhq/ui/sonner';
import { toast } from '@signozhq/ui';
import { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
@@ -6,8 +6,8 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import KeysTab from '../KeysTab';
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: { success: jest.fn(), error: jest.fn() },
}));

View File

@@ -6,8 +6,8 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import ServiceAccountDrawer from '../ServiceAccountDrawer';
jest.mock('@signozhq/ui/drawer', () => ({
...jest.requireActual('@signozhq/ui/drawer'),
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
DrawerWrapper: ({
children,
footer,
@@ -23,10 +23,6 @@ jest.mock('@signozhq/ui/drawer', () => ({
{footer}
</div>
) : null,
}));
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: { success: jest.fn(), error: jest.fn() },
}));
@@ -151,7 +147,7 @@ describe('ServiceAccountDrawer', () => {
});
});
it('adding a role fires POST for the new role and no DELETE for existing roles', async () => {
it('changing roles enables Save; clicking Save sends role add request without delete', async () => {
const roleSpy = jest.fn();
const deleteSpy = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
@@ -171,7 +167,6 @@ describe('ServiceAccountDrawer', () => {
await screen.findByDisplayValue('CI Bot');
// Add signoz-viewer while keeping signoz-admin selected
await user.click(screen.getByLabelText('Roles'));
await user.click(await screen.findByTitle('signoz-viewer'));
@@ -189,43 +184,6 @@ describe('ServiceAccountDrawer', () => {
});
});
it('removing a role fires DELETE for the removed role and no POST', async () => {
const roleSpy = jest.fn();
const deleteSpy = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
rest.post(SA_ROLES_ENDPOINT, async (req, res, ctx) => {
roleSpy(await req.json());
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
}),
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) => {
deleteSpy();
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
}),
);
renderDrawer();
await screen.findByDisplayValue('CI Bot');
// Remove the signoz-admin tag from the multi-select
const adminTag = await screen.findByTitle('signoz-admin');
const removeBtn = adminTag.querySelector(
'.ant-select-selection-item-remove',
) as Element;
await user.click(removeBtn);
const saveBtn = screen.getByRole('button', { name: /Save Changes/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await user.click(saveBtn);
await waitFor(() => {
expect(deleteSpy).toHaveBeenCalled();
expect(roleSpy).not.toHaveBeenCalled();
});
});
it('"Delete Service Account" opens confirm dialog; confirming sends delete request', async () => {
const deleteSpy = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });

View File

@@ -1,5 +1,5 @@
import { ScanSearch } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Badge } from '@signozhq/ui';
import { Tooltip } from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import { ServiceAccountRow } from 'container/ServiceAccountsSettings/utils';

View File

@@ -1,6 +1,6 @@
import { ReactNode } from 'react';
import { Popover } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import dayjs from 'dayjs';

View File

@@ -1,5 +1,5 @@
import { CSSProperties } from 'react';
import { Loader } from '@signozhq/icons';
import { LoadingOutlined } from '@ant-design/icons';
import { Spin, SpinProps } from 'antd';
import { SpinerStyle } from './styles';
@@ -7,14 +7,7 @@ import { SpinerStyle } from './styles';
function Spinner({ size, tip, height, style }: SpinnerProps): JSX.Element {
return (
<SpinerStyle height={height} style={style}>
<Spin
spinning
size={size}
tip={tip}
indicator={
<Loader className="animate-spin" role="img" aria-label="loading" />
}
/>
<Spin spinning size={size} tip={tip} indicator={<LoadingOutlined spin />} />
</SpinerStyle>
);
}

View File

@@ -16,7 +16,7 @@ import {
Typography,
TypographyTextProps,
TypographyTitleProps,
} from '@signozhq/ui/typography';
} from '@signozhq/ui';
import styled, { FlattenSimpleInterpolation } from 'styled-components';
import { IStyledClass } from './types';

View File

@@ -6,7 +6,7 @@ import type {
import { useCallback, useMemo } from 'react';
import { CloseOutlined, MoreOutlined } from '@ant-design/icons';
import { useSortable } from '@dnd-kit/sortable';
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui';
import { flexRender, Header as TanStackHeader } from '@tanstack/react-table';
import cx from 'classnames';
import { ArrowDown, ArrowUp, ArrowUpDown, GripVertical } from 'lucide-react';

View File

@@ -10,15 +10,18 @@ import {
} from 'react';
import type { TableComponents } from 'react-virtuoso';
import { TableVirtuoso, TableVirtuosoHandle } from 'react-virtuoso';
import { Loader } from '@signozhq/icons';
import { LoadingOutlined } from '@ant-design/icons';
import { DndContext, pointerWithin } from '@dnd-kit/core';
import {
horizontalListSortingStrategy,
SortableContext,
} from '@dnd-kit/sortable';
import { ComboboxSimple, ComboboxSimpleItem } from '@signozhq/ui/combobox';
import { TooltipProvider } from '@signozhq/ui/tooltip';
import { Pagination } from '@signozhq/ui/pagination';
import {
ComboboxSimple,
ComboboxSimpleItem,
TooltipProvider,
} from '@signozhq/ui';
import { Pagination } from '@signozhq/ui';
import type { Row } from '@tanstack/react-table';
import {
ColumnDef,
@@ -580,10 +583,7 @@ function TanStackTableInner<TData>(
className={viewStyles.tanstackLoadingOverlay}
data-testid="tanstack-infinite-loader"
>
<Spin
indicator={<Loader className="animate-spin" />}
tip="Loading more..."
/>
<Spin indicator={<LoadingOutlined spin />} tip="Loading more..." />
</div>
)}
{showPagination && pagination && (

View File

@@ -1,6 +1,6 @@
import { ReactNode } from 'react';
import { VirtuosoMockContext } from 'react-virtuoso';
import { TooltipProvider } from '@signozhq/ui/tooltip';
import { TooltipProvider } from '@signozhq/ui';
import { render, RenderResult } from '@testing-library/react';
import { NuqsTestingAdapter, OnUrlUpdateFunction } from 'nuqs/adapters/testing';

View File

@@ -398,7 +398,7 @@ describe('useTableParams (selective URL mode — partial config object)', () =>
.filter(Boolean)
.pop();
expect(lastExpanded).toBeDefined();
expect(JSON.parse(lastExpanded!)).toStrictEqual(
expect(JSON.parse(lastExpanded!)).toEqual(
expect.arrayContaining(['row-1', 'row-2']),
);

View File

@@ -1,4 +1,4 @@
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import { timeItems } from 'container/NewWidget/RightContainer/timeItems';
export const menuItems = timeItems.map((item) => ({

View File

@@ -1,7 +1,7 @@
import { Dispatch, SetStateAction, useCallback, useMemo } from 'react';
import { DownOutlined } from '@ant-design/icons';
import { Button, Dropdown } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Typography } from '@signozhq/ui';
import TimeItems, {
timePreferance,
timePreferenceType,

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