Compare commits

..

1 Commits

Author SHA1 Message Date
Ankit Nayan
27a43dc6d2 enable recommendedsecondary_indices_enable_bulk_filtering 2026-05-21 14:49:00 +05:30
322 changed files with 30658 additions and 33358 deletions

7
.github/CODEOWNERS vendored
View File

@@ -118,9 +118,6 @@ go.mod @therealpandey
/tests/integration/ @therealpandey
# e2e tests
/tests/e2e/ @AshwinBhatkal
# Flagger Owners
/pkg/flagger/ @therealpandey
@@ -165,7 +162,3 @@ go.mod @therealpandey
/frontend/src/lib/dashboard/ @SigNoz/pulse-frontend
/frontend/src/lib/dashboardVariables/ @SigNoz/pulse-frontend
/frontend/src/components/NewSelect/ @SigNoz/pulse-frontend
## Dashboard V2
/frontend/src/pages/DashboardPageV2/ @SigNoz/pulse-frontend
/frontend/src/pages/DashboardsListPageV2/ @SigNoz/pulse-frontend

View File

@@ -123,20 +123,3 @@ jobs:
run: |
go run cmd/enterprise/*.go generate authz
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in authz permissions. Run go run cmd/enterprise/*.go generate authz locally and commit."; exit 1)
web-settings:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v4
- name: go-install
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: generate-web-settings
run: |
go run cmd/enterprise/*.go generate config web-settings
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in web settings schema. Run go run cmd/enterprise/*.go generate config web-settings locally and commit."; exit 1)

View File

@@ -90,26 +90,3 @@ jobs:
run: |
cd frontend && pnpm generate:api
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated api clients. Run pnpm generate:api in frontend/ locally and commit."; exit 1)
web-settings:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v4
- name: node-install
uses: actions/setup-node@v5
with:
node-version: "22"
- name: install-pnpm
uses: pnpm/action-setup@v6
with:
version: 10
- name: install-frontend
run: cd frontend && pnpm install
- name: generate-web-settings
run: |
cd frontend && pnpm generate:config:web-settings
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated web settings types. Run pnpm generate:config:web-settings in frontend/ locally and commit."; exit 1)

View File

@@ -115,7 +115,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
return querier.NewHandler(ps, q, a)
},
func(_ sqlstore.SQLStore, _ dashboard.Module, _ global.Global, _ zeus.Zeus, _ gateway.Gateway, _ licensing.Licensing, _ serviceaccount.Module, _ cloudintegration.Config) (cloudintegration.Module, error) {
func(_ sqlstore.SQLStore, _ global.Global, _ zeus.Zeus, _ gateway.Gateway, _ licensing.Licensing, _ serviceaccount.Module, _ cloudintegration.Config) (cloudintegration.Module, error) {
return implcloudintegration.NewModule(), nil
},
func(c cache.Cache, am alertmanager.Alertmanager, ss sqlstore.SQLStore, ts telemetrystore.TelemetryStore, ms telemetrytypes.MetadataStore, p prometheus.Prometheus, og organization.Getter, rsh rulestatehistory.Module, q querier.Querier, qp queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {

View File

@@ -167,7 +167,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
communityHandler := querier.NewHandler(ps, q, a)
return eequerier.NewHandler(ps, q, communityHandler)
},
func(sqlStore sqlstore.SQLStore, dashboardModule dashboard.Module, global global.Global, zeus zeus.Zeus, gateway gateway.Gateway, licensing licensing.Licensing, serviceAccount serviceaccount.Module, config cloudintegration.Config) (cloudintegration.Module, error) {
func(sqlStore sqlstore.SQLStore, global global.Global, zeus zeus.Zeus, gateway gateway.Gateway, licensing licensing.Licensing, serviceAccount serviceaccount.Module, config cloudintegration.Config) (cloudintegration.Module, error) {
defStore := pkgcloudintegration.NewServiceDefinitionStore()
awsCloudProviderModule, err := implcloudprovider.NewAWSCloudProvider(defStore)
if err != nil {
@@ -179,7 +179,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
cloudintegrationtypes.CloudProviderTypeAzure: azureCloudProviderModule,
}
return implcloudintegration.NewModule(pkgcloudintegration.NewStore(sqlStore), dashboardModule, global, zeus, gateway, licensing, serviceAccount, cloudProvidersMap, config)
return implcloudintegration.NewModule(pkgcloudintegration.NewStore(sqlStore), global, zeus, gateway, licensing, serviceAccount, cloudProvidersMap, config)
},
func(c cache.Cache, am alertmanager.Alertmanager, ss sqlstore.SQLStore, ts telemetrystore.TelemetryStore, ms telemetrytypes.MetadataStore, p prometheus.Prometheus, og organization.Getter, rsh rulestatehistory.Module, q querier.Querier, qp queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
return factory.MustNewNamedMap(signozruler.NewFactory(c, am, ss, ts, ms, p, og, rsh, q, qp, eerules.PrepareTaskFunc, eerules.TestNotification))

View File

@@ -1,61 +0,0 @@
package cmd
import (
"encoding/json"
"os"
"reflect"
"strings"
"github.com/SigNoz/signoz/pkg/web"
"github.com/spf13/cobra"
"github.com/swaggest/jsonschema-go"
)
const webSettingsSchemaPath = "docs/config/web-settings.json"
func registerGenerateConfig(parentCmd *cobra.Command) {
configCmd := &cobra.Command{
Use: "config",
Short: "Generate JSON Schema for config",
}
configCmd.AddCommand(&cobra.Command{
Use: "web-settings",
Short: "Generate JSON Schema for web settings",
RunE: func(currCmd *cobra.Command, args []string) error {
return generateWebSettings()
},
})
parentCmd.AddCommand(configCmd)
}
func generateWebSettings() error {
falseVal := false
noAdditional := jsonschema.SchemaOrBool{TypeBoolean: &falseVal}
reflector := jsonschema.Reflector{}
reflector.DefaultOptions = append(reflector.DefaultOptions,
jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (bool, error) {
if params.Value.Kind() == reflect.Struct {
params.Schema.AdditionalProperties = &noAdditional
}
return false, nil
}),
jsonschema.InterceptDefName(func(t reflect.Type, defaultDefName string) string {
return strings.TrimPrefix(defaultDefName, "Web")
}),
)
schema, err := reflector.Reflect(web.Settings{})
if err != nil {
return err
}
data, err := json.MarshalIndent(schema, "", " ")
if err != nil {
return err
}
return os.WriteFile(webSettingsSchemaPath, append(data, '\n'), 0o600)
}

View File

@@ -17,7 +17,6 @@ func RegisterGenerate(parentCmd *cobra.Command, logger *slog.Logger) {
registerGenerateOpenAPI(generateCmd)
registerGenerateAuthz(generateCmd)
registerGenerateConfig(generateCmd)
parentCmd.AddCommand(generateCmd)
}

View File

@@ -60,14 +60,6 @@ web:
index: index.html
# The directory containing the static build files.
directory: /etc/signoz/web
# Settings exposed to the web.
settings:
posthog:
# Whether to enable PostHog in web.
enabled: true
appcues:
# Whether to enable Appcues in web.
enabled: true
##################### Cache #####################
cache:

View File

@@ -129,8 +129,6 @@ components:
type: string
schedule:
$ref: '#/components/schemas/AlertmanagertypesSchedule'
scope:
type: string
status:
$ref: '#/components/schemas/AlertmanagertypesMaintenanceStatus'
updatedAt:
@@ -274,8 +272,6 @@ components:
type: string
schedule:
$ref: '#/components/schemas/AlertmanagertypesSchedule'
scope:
type: string
required:
- name
- schedule
@@ -2693,6 +2689,7 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesClusterRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -2762,6 +2759,7 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesDaemonSetRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -2831,6 +2829,7 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesDeploymentRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -2909,6 +2908,7 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesHostRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -2984,6 +2984,7 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesJobRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -3031,6 +3032,7 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesNamespaceRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -3108,6 +3110,7 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesNodeRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -3206,6 +3209,7 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesPodRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -3550,6 +3554,7 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesStatefulSetRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -3610,6 +3615,7 @@ components:
records:
items:
$ref: '#/components/schemas/InframonitoringtypesVolumeRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
@@ -5645,19 +5651,6 @@ components:
type: object
Sigv4SigV4Config:
type: object
SpantypesEvent:
properties:
attributeMap:
additionalProperties: {}
type: object
isError:
type: boolean
name:
type: string
timeUnixNano:
minimum: 0
type: integer
type: object
SpantypesFieldContext:
enum:
- attribute
@@ -5672,44 +5665,6 @@ components:
required:
- items
type: object
SpantypesGettableWaterfallTrace:
properties:
aggregations:
items:
$ref: '#/components/schemas/SpantypesSpanAggregationResult'
nullable: true
type: array
endTimestampMillis:
minimum: 0
type: integer
hasMissingSpans:
type: boolean
hasMore:
type: boolean
rootServiceEntryPoint:
type: string
rootServiceName:
type: string
spans:
items:
$ref: '#/components/schemas/SpantypesWaterfallSpan'
nullable: true
type: array
startTimestampMillis:
minimum: 0
type: integer
totalErrorSpansCount:
minimum: 0
type: integer
totalSpansCount:
minimum: 0
type: integer
uncollapsedSpans:
items:
type: string
nullable: true
type: array
type: object
SpantypesPostableSpanMapper:
properties:
config:
@@ -5737,50 +5692,6 @@ components:
- name
- condition
type: object
SpantypesPostableWaterfall:
properties:
aggregations:
items:
$ref: '#/components/schemas/SpantypesSpanAggregation'
nullable: true
type: array
limit:
minimum: 0
type: integer
selectedSpanId:
type: string
uncollapsedSpans:
items:
type: string
nullable: true
type: array
type: object
SpantypesSpanAggregation:
properties:
aggregation:
$ref: '#/components/schemas/SpantypesSpanAggregationType'
field:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
type: object
SpantypesSpanAggregationResult:
properties:
aggregation:
$ref: '#/components/schemas/SpantypesSpanAggregationType'
field:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
value:
additionalProperties:
minimum: 0
type: integer
nullable: true
type: object
type: object
SpantypesSpanAggregationType:
enum:
- span_count
- execution_time_percentage
- duration
type: string
SpantypesSpanMapper:
properties:
config:
@@ -5911,78 +5822,6 @@ components:
nullable: true
type: string
type: object
SpantypesWaterfallSpan:
properties:
attributes:
additionalProperties: {}
nullable: true
type: object
db_name:
type: string
db_operation:
type: string
duration_nano:
minimum: 0
type: integer
events:
items:
$ref: '#/components/schemas/SpantypesEvent'
nullable: true
type: array
external_http_method:
type: string
external_http_url:
type: string
flags:
minimum: 0
type: integer
has_children:
type: boolean
has_error:
type: boolean
http_host:
type: string
http_method:
type: string
http_url:
type: string
is_remote:
type: string
kind_string:
type: string
level:
minimum: 0
type: integer
name:
type: string
parent_span_id:
type: string
resource:
additionalProperties:
type: string
nullable: true
type: object
response_status_code:
type: string
span_id:
type: string
status_code:
type: integer
status_code_string:
type: string
status_message:
type: string
sub_tree_node_count:
minimum: 0
type: integer
time_unix:
minimum: 0
type: integer
trace_id:
type: string
trace_state:
type: string
type: object
TelemetrytypesFieldContext:
enum:
- metric
@@ -6075,6 +5914,179 @@ components:
TimeDuration:
format: int64
type: integer
TracedetailtypesEvent:
properties:
attributeMap:
additionalProperties: {}
type: object
isError:
type: boolean
name:
type: string
timeUnixNano:
minimum: 0
type: integer
type: object
TracedetailtypesGettableWaterfallTrace:
properties:
aggregations:
items:
$ref: '#/components/schemas/TracedetailtypesSpanAggregationResult'
nullable: true
type: array
endTimestampMillis:
minimum: 0
type: integer
hasMissingSpans:
type: boolean
hasMore:
type: boolean
rootServiceEntryPoint:
type: string
rootServiceName:
type: string
serviceNameToTotalDurationMap:
additionalProperties:
minimum: 0
type: integer
nullable: true
type: object
spans:
items:
$ref: '#/components/schemas/TracedetailtypesWaterfallSpan'
nullable: true
type: array
startTimestampMillis:
minimum: 0
type: integer
totalErrorSpansCount:
minimum: 0
type: integer
totalSpansCount:
minimum: 0
type: integer
uncollapsedSpans:
items:
type: string
nullable: true
type: array
type: object
TracedetailtypesPostableWaterfall:
properties:
aggregations:
items:
$ref: '#/components/schemas/TracedetailtypesSpanAggregation'
nullable: true
type: array
limit:
minimum: 0
type: integer
selectedSpanId:
type: string
uncollapsedSpans:
items:
type: string
nullable: true
type: array
type: object
TracedetailtypesSpanAggregation:
properties:
aggregation:
$ref: '#/components/schemas/TracedetailtypesSpanAggregationType'
field:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
type: object
TracedetailtypesSpanAggregationResult:
properties:
aggregation:
$ref: '#/components/schemas/TracedetailtypesSpanAggregationType'
field:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
value:
additionalProperties:
minimum: 0
type: integer
nullable: true
type: object
type: object
TracedetailtypesSpanAggregationType:
enum:
- span_count
- execution_time_percentage
- duration
type: string
TracedetailtypesWaterfallSpan:
properties:
attributes:
additionalProperties: {}
nullable: true
type: object
db_name:
type: string
db_operation:
type: string
duration_nano:
minimum: 0
type: integer
events:
items:
$ref: '#/components/schemas/TracedetailtypesEvent'
nullable: true
type: array
external_http_method:
type: string
external_http_url:
type: string
flags:
minimum: 0
type: integer
has_children:
type: boolean
has_error:
type: boolean
http_host:
type: string
http_method:
type: string
http_url:
type: string
is_remote:
type: string
kind_string:
type: string
level:
minimum: 0
type: integer
name:
type: string
parent_span_id:
type: string
resource:
additionalProperties:
type: string
nullable: true
type: object
response_status_code:
type: string
span_id:
type: string
status_code:
type: integer
status_code_string:
type: string
status_message:
type: string
sub_tree_node_count:
minimum: 0
type: integer
time_unix:
minimum: 0
type: integer
trace_id:
type: string
trace_state:
type: string
type: object
TypesAlertStatus:
properties:
inhibitedBy:
@@ -18894,7 +18906,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/SpantypesPostableWaterfall'
$ref: '#/components/schemas/TracedetailtypesPostableWaterfall'
responses:
"200":
content:
@@ -18902,7 +18914,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/SpantypesGettableWaterfallTrace'
$ref: '#/components/schemas/TracedetailtypesGettableWaterfallTrace'
status:
type: string
required:
@@ -18948,77 +18960,6 @@ paths:
summary: Get waterfall view for a trace
tags:
- tracedetail
/api/v4/traces/{traceID}/waterfall:
post:
deprecated: false
description: 'Two-step fetch: minimal fields for all spans to build the tree,
full fields only for the visible window. Aggregations are not included in
the response.'
operationId: GetWaterfallV4
parameters:
- in: path
name: traceID
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SpantypesPostableWaterfall'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/SpantypesGettableWaterfallTrace'
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
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get waterfall view for a trace (OOM-safe)
tags:
- tracedetail
/api/v5/query_range:
post:
deprecated: false

View File

@@ -1,42 +0,0 @@
{
"required": [
"posthog",
"appcues"
],
"additionalProperties": false,
"definitions": {
"Appcues": {
"required": [
"enabled"
],
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
}
},
"type": "object"
},
"Posthog": {
"required": [
"enabled"
],
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
}
},
"type": "object"
}
},
"properties": {
"appcues": {
"$ref": "#/definitions/Appcues"
},
"posthog": {
"$ref": "#/definitions/Posthog"
}
},
"type": "object"
}

View File

@@ -54,6 +54,11 @@ func (provider *awscloudprovider) GetServiceDefinition(ctx context.Context, serv
return nil, err
}
// override cloud integration dashboard id
for index, dashboard := range serviceDef.Assets.Dashboards {
serviceDef.Assets.Dashboards[index].ID = cloudintegrationtypes.GetCloudIntegrationDashboardID(cloudintegrationtypes.CloudProviderTypeAWS, serviceID.StringValue(), dashboard.ID)
}
return serviceDef, nil
}

View File

@@ -38,6 +38,11 @@ func (provider *azurecloudprovider) GetServiceDefinition(ctx context.Context, se
return nil, err
}
// override cloud integration dashboard id.
for index, dashboard := range serviceDef.Assets.Dashboards {
serviceDef.Assets.Dashboards[index].ID = cloudintegrationtypes.GetCloudIntegrationDashboardID(cloudintegrationtypes.CloudProviderTypeAzure, serviceID.StringValue(), dashboard.ID)
}
return serviceDef, nil
}

View File

@@ -3,6 +3,7 @@ package implcloudintegration
import (
"context"
"fmt"
"sort"
"time"
"github.com/SigNoz/signoz/pkg/errors"
@@ -10,7 +11,6 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
@@ -23,7 +23,6 @@ import (
type module struct {
store cloudintegrationtypes.Store
dashboardModule dashboard.Module
gateway gateway.Gateway
zeus zeus.Zeus
licensing licensing.Licensing
@@ -35,7 +34,6 @@ type module struct {
func NewModule(
store cloudintegrationtypes.Store,
dashboardModule dashboard.Module,
global global.Global,
zeus zeus.Zeus,
gateway gateway.Gateway,
@@ -46,7 +44,6 @@ func NewModule(
) (cloudintegration.Module, error) {
return &module{
store: store,
dashboardModule: dashboardModule,
global: global,
zeus: zeus,
gateway: gateway,
@@ -257,41 +254,7 @@ func (module *module) DisconnectAccount(ctx context.Context, orgID valuer.UUID,
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
return module.store.RunInTx(ctx, func(ctx context.Context) error {
services, err := module.store.ListServices(ctx, accountID)
if err != nil {
return err
}
sharedServices, err := module.store.ListSharedServices(ctx, orgID, provider, accountID)
if err != nil {
return err
}
for _, svc := range services {
svcCfg, err := cloudintegrationtypes.NewServiceConfigFromJSON(provider, svc.Config)
if err != nil {
return err
}
if !svcCfg.IsMetricsEnabled(provider) {
continue
}
if cloudintegrationtypes.IsServiceSharedWithMetricsEnabled(provider, sharedServices[svc.Type]) {
continue
}
if err := module.deprovisionDashboards(ctx, orgID, provider, svc.Type); err != nil {
return err
}
}
if err := module.store.DeleteServicesByCloudIntegrationID(ctx, orgID, accountID); err != nil {
return err
}
return module.store.RemoveAccount(ctx, orgID, accountID, provider)
})
return module.store.RemoveAccount(ctx, orgID, accountID, provider)
}
func (module *module) ListServicesMetadata(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, integrationID valuer.UUID) ([]*cloudintegrationtypes.ServiceMetadata, error) {
@@ -368,16 +331,12 @@ func (module *module) GetService(ctx context.Context, orgID valuer.UUID, service
integrationService = cloudintegrationtypes.NewCloudIntegrationServiceFromStorable(storedService, serviceConfig)
}
if err := module.enrichDashboardIDs(ctx, orgID, provider, serviceID, serviceDefinition); err != nil {
return nil, err
}
}
return cloudintegrationtypes.NewService(*serviceDefinition, integrationService), nil
}
func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -398,21 +357,10 @@ func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, crea
return err
}
metricsEnabled := service.Config.IsMetricsEnabled(provider)
storableService := cloudintegrationtypes.NewStorableCloudIntegrationService(service, string(configJSON))
return module.store.RunInTx(ctx, func(ctx context.Context) error {
if err := module.store.CreateService(ctx, storableService); err != nil {
return err
}
if metricsEnabled {
return module.provisionDashboards(ctx, orgID, createdBy, creator, provider, service, serviceDefinition)
}
return nil
})
return module.store.CreateService(ctx, cloudintegrationtypes.NewStorableCloudIntegrationService(service, string(configJSON)))
}
func (module *module) UpdateService(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, integrationService *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
func (module *module) UpdateService(ctx context.Context, orgID valuer.UUID, integrationService *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -433,28 +381,43 @@ func (module *module) UpdateService(ctx context.Context, orgID valuer.UUID, crea
return err
}
metricsEnabled := integrationService.Config.IsMetricsEnabled(provider)
storableService := cloudintegrationtypes.NewStorableCloudIntegrationService(integrationService, string(configJSON))
return module.store.RunInTx(ctx, func(ctx context.Context) error {
if err := module.store.UpdateService(ctx, storableService); err != nil {
return err
}
return module.store.UpdateService(ctx, storableService)
}
if metricsEnabled {
return module.provisionDashboards(ctx, orgID, createdBy, creator, provider, integrationService, serviceDefinition)
}
func (module *module) GetDashboardByID(ctx context.Context, orgID valuer.UUID, id string) (*dashboardtypes.Dashboard, error) {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
sharedServices, err := module.store.ListSharedServices(ctx, orgID, provider, integrationService.CloudIntegrationID)
if err != nil {
return err
}
if cloudintegrationtypes.IsServiceSharedWithMetricsEnabled(provider, sharedServices[integrationService.Type]) {
return nil
}
_, _, _, err = cloudintegrationtypes.ParseCloudIntegrationDashboardID(id)
if err != nil {
return nil, err
}
return module.deprovisionDashboards(ctx, orgID, provider, integrationService.Type)
})
allDashboards, err := module.listDashboards(ctx, orgID)
if err != nil {
return nil, err
}
for _, d := range allDashboards {
if d.ID == id {
return d, nil
}
}
return nil, errors.New(errors.TypeNotFound, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "cloud integration dashboard not found")
}
func (module *module) ListDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
return module.listDashboards(ctx, orgID)
}
func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
@@ -530,73 +493,52 @@ func (module *module) getOrCreateAPIKey(ctx context.Context, orgID valuer.UUID,
return factorAPIKey.Key, nil
}
// provisionDashboards creates dashboard and integration_dashboard rows for each dashboard in the service definition.
// Must be called within a transaction (ctx carries the tx).
func (module *module) provisionDashboards(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, provider cloudintegrationtypes.CloudProviderType, service *cloudintegrationtypes.CloudIntegrationService, serviceDefinition *cloudintegrationtypes.ServiceDefinition) error {
// TODO: DB calls are in for loop, can be optimized later.
for _, dashboard := range serviceDefinition.Assets.Dashboards {
slug := cloudintegrationtypes.IntegrationDashboardSlug(provider, service.Type, dashboard.ID)
func (module *module) listDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
var allDashboards []*dashboardtypes.Dashboard
existing, err := module.store.GetIntegrationDashboardBySlug(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return err
}
if existing != nil {
continue
}
createdDashboard, err := module.dashboardModule.Create(ctx, orgID, createdBy, creator, dashboardtypes.SourceIntegration, dashboardtypes.PostableDashboard(dashboard.Definition))
for provider := range module.cloudProvidersMap {
cloudProvider, err := module.getCloudProvider(provider)
if err != nil {
return err
return nil, err
}
integrationDashboard := cloudintegrationtypes.NewStorableIntegrationDashboard(createdDashboard.ID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
if err := module.store.CreateIntegrationDashboard(ctx, integrationDashboard); err != nil {
return err
}
}
return nil
}
// deprovisionDashboards deletes all dashboard and integration_dashboard rows for the given service.
// make sure to call this within a transaction.
func (module *module) deprovisionDashboards(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, serviceID cloudintegrationtypes.ServiceID) error {
slugPrefix := cloudintegrationtypes.IntegrationDashboardSlugPrefix(provider, serviceID)
rows, err := module.store.ListIntegrationDashboardsBySlugPrefix(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slugPrefix)
if err != nil {
return err
}
for _, row := range rows {
dashID, err := valuer.NewUUID(row.DashboardID)
connectedAccounts, err := module.store.ListConnectedAccounts(ctx, orgID, provider)
if err != nil {
return err
return nil, err
}
if err := module.store.DeleteIntegrationDashboardBySlug(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, row.Slug); err != nil {
return err
}
if err := module.dashboardModule.DeleteUnsafe(ctx, orgID, dashID); err != nil {
return err
}
}
return nil
}
// enrichDashboardIDs replaces the raw dashboard name in each Dashboard.ID with the provisioned UUID.
// TODO: remove this hack and send idiomatic response to client.
func (module *module) enrichDashboardIDs(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, serviceID cloudintegrationtypes.ServiceID, serviceDefinition *cloudintegrationtypes.ServiceDefinition) error {
for i, d := range serviceDefinition.Assets.Dashboards {
slug := cloudintegrationtypes.IntegrationDashboardSlug(provider, serviceID, d.ID)
row, err := module.store.GetIntegrationDashboardBySlug(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
continue
for _, storableAccount := range connectedAccounts {
storedServices, err := module.store.ListServices(ctx, storableAccount.ID)
if err != nil {
return nil, err
}
for _, storedSvc := range storedServices {
serviceConfig, err := cloudintegrationtypes.NewServiceConfigFromJSON(provider, storedSvc.Config)
if err != nil || !serviceConfig.IsMetricsEnabled(provider) {
continue
}
svcDef, err := cloudProvider.GetServiceDefinition(ctx, storedSvc.Type)
if err != nil || svcDef == nil {
continue
}
dashboards := cloudintegrationtypes.GetDashboardsFromAssets(
storedSvc.Type.StringValue(),
orgID,
provider,
storableAccount.CreatedAt,
svcDef.Assets,
)
allDashboards = append(allDashboards, dashboards...)
}
return err
}
serviceDefinition.Assets.Dashboards[i].ID = row.DashboardID
}
return nil
sort.Slice(allDashboards, func(i, j int) bool {
return allDashboards[i].ID < allDashboards[j].ID
})
return allDashboards, nil
}

View File

@@ -162,11 +162,24 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
}
return module.delete(ctx, orgID, id)
}
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
err := module.store.DeletePublic(ctx, id.String())
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return err
}
func (module *module) DeleteUnsafe(ctx context.Context, orgID, id valuer.UUID) error {
return module.delete(ctx, orgID, id)
err = module.store.Delete(ctx, orgID, id)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
}
func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashboardID valuer.UUID) error {
@@ -208,8 +221,8 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
return stats, nil
}
func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, source dashboardtypes.Source, data dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Create(ctx, orgID, createdBy, creator, source, data)
func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, data dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Create(ctx, orgID, createdBy, creator, data)
}
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
@@ -231,12 +244,3 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.U
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, isAdmin, lock)
}
func (module *module) delete(ctx context.Context, orgID, id valuer.UUID) error {
return module.store.RunInTx(ctx, func(ctx context.Context) error {
if err := module.store.DeletePublic(ctx, id.String()); err != nil && !errors.Ast(err, errors.TypeNotFound) {
return err
}
return module.store.Delete(ctx, orgID, id)
})
}

View File

@@ -89,15 +89,6 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
Route: "",
})
useDashboardV2 := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseDashboardV2, evalCtx)
featureSet = append(featureSet, &licensetypes.Feature{
Name: valuer.NewString(flagger.FeatureUseDashboardV2.String()),
Active: useDashboardV2,
Usage: 0,
UsageLimit: -1,
Route: "",
})
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {

View File

@@ -94,19 +94,6 @@
}
})();
</script>
<script type="application/json" id="signoz-boot-settings">
[[.Settings]]
</script>
<script>
try {
var _el = document.getElementById('signoz-boot-settings');
window.signozBootData = {
settings: _el ? JSON.parse(_el.textContent) : null,
};
} catch (e) {
window.signozBootData = { settings: null };
}
</script>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
@@ -148,10 +135,7 @@
</script>
<script>
var APPCUES_APP_ID = '<%- APPCUES_APP_ID %>';
var appcuesSettings =
((window.signozBootData || {}).settings || {}).appcues || {};
var appcuesEnabled = appcuesSettings.enabled !== false;
if (APPCUES_APP_ID && appcuesEnabled) {
if (APPCUES_APP_ID) {
(function (d, t) {
var a = d.createElement(t);
a.async = 1;

View File

@@ -47,10 +47,10 @@ const config: Config.InitialOptions = {
transformIgnorePatterns: [
// @chenglou/pretext is ESM-only; @signozhq/ui pulls it in via text-ellipsis.
// Pattern 1: allow .pnpm virtual store through (handled by pattern 2), plus root-level ESM packages.
'node_modules/(?!(\\.pnpm|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou/pretext|@signozhq/design-tokens|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard)/)',
'node_modules/(?!(\\.pnpm|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou/pretext|@signozhq/design-tokens|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid)/)',
// Pattern 2: pnpm virtual store — ignore everything except ESM-only packages.
// pnpm encodes scoped packages as @scope+name@version, so match on scope prefix.
'node_modules/\\.pnpm/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard)[^/]*/node_modules)',
'node_modules/\\.pnpm/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid)[^/]*/node_modules)',
],
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],

View File

@@ -24,8 +24,7 @@
"commitlint": "commitlint --edit $1",
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
"generate:config:web-settings": "json2ts ../docs/config/web-settings.json -o src/types/generated/webSettings.ts --style.useTabs --style.tabWidth=1 --style.singleQuote --bannerComment '/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM docs/config/web-settings.json */'"
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh"
},
"engines": {
"node": ">=22.0.0",
@@ -50,7 +49,7 @@
"@signozhq/design-tokens": "2.1.4",
"@signozhq/icons": "0.4.0",
"@signozhq/resizable": "0.0.2",
"@signozhq/ui": "0.0.22",
"@signozhq/ui": "0.0.19",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",
"@uiw/codemirror-theme-copilot": "4.23.11",
@@ -161,8 +160,8 @@
"@testing-library/user-event": "14.4.3",
"@types/color": "^3.0.3",
"@types/crypto-js": "4.2.2",
"@types/d3-hierarchy": "1.1.11",
"@types/event-source-polyfill": "^1.0.0",
"@types/d3-hierarchy": "1.1.11",
"@types/history": "4.7.11",
"@types/jest": "30.0.0",
"@types/lodash-es": "^4.17.4",
@@ -188,7 +187,6 @@
"is-ci": "^3.0.1",
"jest-environment-jsdom": "29.7.0",
"jest-styled-components": "^7.2.0",
"json-schema-to-typescript": "^15.0.4",
"lint-staged": "^17.0.4",
"msw": "1.3.2",
"orval": "8.9.1",
@@ -243,4 +241,4 @@
"tmp": "0.2.4",
"vite": "npm:rolldown-vite@7.3.1"
}
}
}

View File

@@ -14,11 +14,8 @@
*/
const BANNED_COMPONENTS = {
Typography:
'Use @signozhq/ui/typography Typography instead of antd Typography.',
Switch: 'Use @signozhq/ui/switch Switch instead of antd Switch.',
Typography: 'Use @signozhq/ui Typography instead of antd Typography.',
Badge: 'Use @signozhq/ui/badge instead of antd Badge.',
Progress: 'Use @signozhq/ui/progress instead of antd Progress.',
};
export default {

View File

@@ -77,8 +77,8 @@ importers:
specifier: 0.0.2
version: 0.0.2(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@signozhq/ui':
specifier: 0.0.22
version: 0.0.22(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
specifier: 0.0.19
version: 0.0.19(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
'@tanstack/react-table':
specifier: 8.21.3
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -449,9 +449,6 @@ importers:
jest-styled-components:
specifier: ^7.2.0
version: 7.2.0(styled-components@5.3.11(react-dom@18.2.0(react@18.2.0))(react-is@19.2.6)(react@18.2.0))
json-schema-to-typescript:
specifier: ^15.0.4
version: 15.0.4
lint-staged:
specifier: ^17.0.4
version: 17.0.4
@@ -460,7 +457,7 @@ importers:
version: 1.3.2(typescript@5.9.3)
orval:
specifier: 8.9.1
version: 8.9.1(prettier@3.8.3)(typescript@5.9.3)
version: 8.9.1(typescript@5.9.3)
oxfmt:
specifier: 0.47.0
version: 0.47.0
@@ -548,10 +545,6 @@ packages:
peerDependencies:
react: '>=16.9.0'
'@apidevtools/json-schema-ref-parser@11.9.3':
resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==}
engines: {node: '>= 16'}
'@babel/code-frame@7.29.0':
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'}
@@ -1998,9 +1991,6 @@ packages:
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@jsdevtools/ono@7.1.3':
resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
'@keyv/bigmap@1.3.1':
resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==}
engines: {node: '>= 18'}
@@ -3279,8 +3269,8 @@ packages:
peerDependencies:
react: ^18.2.0
'@signozhq/ui@0.0.22':
resolution: {integrity: sha512-CJDyA4H+uXG/U2/d7/nRMNY6WIW0YWc843mfzUQALjm+xOhbO4T+qt67THjV4s1wTMs1cZLkmScbMddf+hXLIQ==}
'@signozhq/ui@0.0.19':
resolution: {integrity: sha512-2q6aRxN/PR4PlR2xJZAREEuvLPiDFggfFKzCW2Z5vHVVbrgnvZHWD1jPUuwszfEg0ceH3UvkwqceO7wN4uRJAA==}
peerDependencies:
'@signozhq/icons': 0.3.0
react: ^18.2.0
@@ -3861,6 +3851,27 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
'@webassemblyjs/ast@1.14.1':
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
'@webassemblyjs/floating-point-hex-parser@1.13.2':
resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==}
'@webassemblyjs/helper-api-error@1.13.2':
resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==}
'@webassemblyjs/helper-buffer@1.14.1':
resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==}
'@webassemblyjs/helper-numbers@1.13.2':
resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==}
'@webassemblyjs/helper-wasm-bytecode@1.13.2':
resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==}
'@webassemblyjs/helper-wasm-section@1.14.1':
resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==}
'@xmldom/xmldom@0.8.13':
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
engines: {node: '>=10.0.0'}
@@ -6076,11 +6087,6 @@ packages:
json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
json-schema-to-typescript@15.0.4:
resolution: {integrity: sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==}
engines: {node: '>=16.0.0'}
hasBin: true
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
@@ -7119,11 +7125,6 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
prettier@3.8.3:
resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==}
engines: {node: '>=14'}
hasBin: true
pretty-format@27.5.1:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -9064,12 +9065,6 @@ snapshots:
resize-observer-polyfill: 1.5.1
throttle-debounce: 5.0.0
'@apidevtools/json-schema-ref-parser@11.9.3':
dependencies:
'@jsdevtools/ono': 7.1.3
'@types/json-schema': 7.0.15
js-yaml: 4.1.1
'@babel/code-frame@7.29.0':
dependencies:
'@babel/helper-validator-identifier': 7.28.5
@@ -10824,8 +10819,6 @@ snapshots:
'@jridgewell/sourcemap-codec': 1.5.5
optional: true
'@jsdevtools/ono@7.1.3': {}
'@keyv/bigmap@1.3.1(keyv@5.6.0)':
dependencies:
hashery: 1.5.1
@@ -12041,7 +12034,7 @@ snapshots:
- react-dom
- tailwindcss
'@signozhq/ui@0.0.22(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
'@signozhq/ui@0.0.19(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
dependencies:
'@chenglou/pretext': 0.0.5
'@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -15402,18 +15395,6 @@ snapshots:
json-parse-even-better-errors@2.3.1: {}
json-schema-to-typescript@15.0.4:
dependencies:
'@apidevtools/json-schema-ref-parser': 11.9.3
'@types/json-schema': 7.0.15
'@types/lodash': 4.17.24
is-glob: 4.0.3
js-yaml: 4.1.1
lodash: 4.18.1
minimist: 1.2.8
prettier: 3.8.3
tinyglobby: 0.2.15
json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {}
@@ -16330,7 +16311,7 @@ snapshots:
strip-ansi: 6.0.1
wcwidth: 1.0.1
orval@8.9.1(prettier@3.8.3)(typescript@5.9.3):
orval@8.9.1(typescript@5.9.3):
dependencies:
'@commander-js/extra-typings': 14.0.0(commander@14.0.2)
'@orval/angular': 8.9.1(typescript@5.9.3)
@@ -16361,8 +16342,6 @@ snapshots:
typedoc: 0.28.19(typescript@5.9.3)
typedoc-plugin-coverage: 4.0.2(typedoc@0.28.19(typescript@5.9.3))
typedoc-plugin-markdown: 4.11.0(typedoc@0.28.19(typescript@5.9.3))
optionalDependencies:
prettier: 3.8.3
transitivePeerDependencies:
- '@faker-js/faker'
- supports-color
@@ -16623,8 +16602,6 @@ snapshots:
prelude-ls@1.2.1: {}
prettier@3.8.3: {}
pretty-format@27.5.1:
dependencies:
ansi-regex: 5.0.1

View File

@@ -35,7 +35,6 @@ import { PreferenceContextProvider } from 'providers/preferences/context/Prefere
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { LicenseStatus } from 'types/api/licensesV3/getActive';
import { extractDomain } from 'utils/app';
import { bootSettings } from 'utils/bootData';
import { Home } from './pageComponents';
import PrivateRoute from './Private';
@@ -333,7 +332,7 @@ function App(): JSX.Element {
useEffect(() => {
if (isCloudUser || isEnterpriseSelfHostedUser) {
if (bootSettings.posthog.enabled && process.env.POSTHOG_KEY) {
if (process.env.POSTHOG_KEY) {
posthog.init(process.env.POSTHOG_KEY, {
api_host: 'https://us.i.posthog.com',
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well

View File

@@ -225,10 +225,6 @@ export interface AlertmanagertypesPlannedMaintenanceDTO {
*/
name: string;
schedule: AlertmanagertypesScheduleDTO;
/**
* @type string
*/
scope?: string;
status: AlertmanagertypesMaintenanceStatusDTO;
/**
* @type string
@@ -1718,10 +1714,6 @@ export interface AlertmanagertypesPostablePlannedMaintenanceDTO {
*/
name: string;
schedule: AlertmanagertypesScheduleDTO;
/**
* @type string
*/
scope?: string;
}
export interface AlertmanagertypesPostableRoutePolicyDTO {
@@ -3496,9 +3488,9 @@ export interface InframonitoringtypesClustersDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array
* @type array,null
*/
records: InframonitoringtypesClusterRecordDTO[];
records: InframonitoringtypesClusterRecordDTO[] | null;
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3574,9 +3566,9 @@ export interface InframonitoringtypesDaemonSetsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array
* @type array,null
*/
records: InframonitoringtypesDaemonSetRecordDTO[];
records: InframonitoringtypesDaemonSetRecordDTO[] | null;
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3652,9 +3644,9 @@ export interface InframonitoringtypesDeploymentsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array
* @type array,null
*/
records: InframonitoringtypesDeploymentRecordDTO[];
records: InframonitoringtypesDeploymentRecordDTO[] | null;
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3738,9 +3730,9 @@ export interface InframonitoringtypesHostsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array
* @type array,null
*/
records: InframonitoringtypesHostRecordDTO[];
records: InframonitoringtypesHostRecordDTO[] | null;
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3824,9 +3816,9 @@ export interface InframonitoringtypesJobsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array
* @type array,null
*/
records: InframonitoringtypesJobRecordDTO[];
records: InframonitoringtypesJobRecordDTO[] | null;
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3874,9 +3866,9 @@ export interface InframonitoringtypesNamespacesDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array
* @type array,null
*/
records: InframonitoringtypesNamespaceRecordDTO[];
records: InframonitoringtypesNamespaceRecordDTO[] | null;
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -3941,9 +3933,9 @@ export interface InframonitoringtypesNodesDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array
* @type array,null
*/
records: InframonitoringtypesNodeRecordDTO[];
records: InframonitoringtypesNodeRecordDTO[] | null;
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -4025,9 +4017,9 @@ export interface InframonitoringtypesPodsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array
* @type array,null
*/
records: InframonitoringtypesPodRecordDTO[];
records: InframonitoringtypesPodRecordDTO[] | null;
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -4445,9 +4437,9 @@ export interface InframonitoringtypesStatefulSetsDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array
* @type array,null
*/
records: InframonitoringtypesStatefulSetRecordDTO[];
records: InframonitoringtypesStatefulSetRecordDTO[] | null;
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -4514,9 +4506,9 @@ export interface InframonitoringtypesVolumesDTO {
*/
endTimeBeforeRetention: boolean;
/**
* @type array
* @type array,null
*/
records: InframonitoringtypesVolumeRecordDTO[];
records: InframonitoringtypesVolumeRecordDTO[] | null;
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
@@ -6663,28 +6655,6 @@ export interface ServiceaccounttypesUpdatableFactorAPIKeyDTO {
name: string;
}
export type SpantypesEventDTOAttributeMap = { [key: string]: unknown };
export interface SpantypesEventDTO {
/**
* @type object
*/
attributeMap?: SpantypesEventDTOAttributeMap;
/**
* @type boolean
*/
isError?: boolean;
/**
* @type string
*/
name?: string;
/**
* @type integer
* @minimum 0
*/
timeUnixNano?: number;
}
export enum SpantypesFieldContextDTO {
attribute = 'attribute',
resource = 'resource',
@@ -6751,219 +6721,6 @@ export interface SpantypesGettableSpanMapperGroupsDTO {
items: SpantypesSpanMapperGroupDTO[];
}
export enum SpantypesSpanAggregationTypeDTO {
span_count = 'span_count',
execution_time_percentage = 'execution_time_percentage',
duration = 'duration',
}
export type SpantypesSpanAggregationResultDTOValueAnyOf = {
[key: string]: number;
};
/**
* @nullable
*/
export type SpantypesSpanAggregationResultDTOValue =
SpantypesSpanAggregationResultDTOValueAnyOf | null;
export interface SpantypesSpanAggregationResultDTO {
aggregation?: SpantypesSpanAggregationTypeDTO;
field?: TelemetrytypesTelemetryFieldKeyDTO;
/**
* @type object,null
*/
value?: SpantypesSpanAggregationResultDTOValue;
}
export type SpantypesWaterfallSpanDTOAttributesAnyOf = {
[key: string]: unknown;
};
/**
* @nullable
*/
export type SpantypesWaterfallSpanDTOAttributes =
SpantypesWaterfallSpanDTOAttributesAnyOf | null;
export type SpantypesWaterfallSpanDTOResourceAnyOf = { [key: string]: string };
/**
* @nullable
*/
export type SpantypesWaterfallSpanDTOResource =
SpantypesWaterfallSpanDTOResourceAnyOf | null;
export interface SpantypesWaterfallSpanDTO {
/**
* @type object,null
*/
attributes?: SpantypesWaterfallSpanDTOAttributes;
/**
* @type string
*/
db_name?: string;
/**
* @type string
*/
db_operation?: string;
/**
* @type integer
* @minimum 0
*/
duration_nano?: number;
/**
* @type array,null
*/
events?: SpantypesEventDTO[] | null;
/**
* @type string
*/
external_http_method?: string;
/**
* @type string
*/
external_http_url?: string;
/**
* @type integer
* @minimum 0
*/
flags?: number;
/**
* @type boolean
*/
has_children?: boolean;
/**
* @type boolean
*/
has_error?: boolean;
/**
* @type string
*/
http_host?: string;
/**
* @type string
*/
http_method?: string;
/**
* @type string
*/
http_url?: string;
/**
* @type string
*/
is_remote?: string;
/**
* @type string
*/
kind_string?: string;
/**
* @type integer
* @minimum 0
*/
level?: number;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
parent_span_id?: string;
/**
* @type object,null
*/
resource?: SpantypesWaterfallSpanDTOResource;
/**
* @type string
*/
response_status_code?: string;
/**
* @type string
*/
span_id?: string;
/**
* @type integer
*/
status_code?: number;
/**
* @type string
*/
status_code_string?: string;
/**
* @type string
*/
status_message?: string;
/**
* @type integer
* @minimum 0
*/
sub_tree_node_count?: number;
/**
* @type integer
* @minimum 0
*/
time_unix?: number;
/**
* @type string
*/
trace_id?: string;
/**
* @type string
*/
trace_state?: string;
}
export interface SpantypesGettableWaterfallTraceDTO {
/**
* @type array,null
*/
aggregations?: SpantypesSpanAggregationResultDTO[] | null;
/**
* @type integer
* @minimum 0
*/
endTimestampMillis?: number;
/**
* @type boolean
*/
hasMissingSpans?: boolean;
/**
* @type boolean
*/
hasMore?: boolean;
/**
* @type string
*/
rootServiceEntryPoint?: string;
/**
* @type string
*/
rootServiceName?: string;
/**
* @type array,null
*/
spans?: SpantypesWaterfallSpanDTO[] | null;
/**
* @type integer
* @minimum 0
*/
startTimestampMillis?: number;
/**
* @type integer
* @minimum 0
*/
totalErrorSpansCount?: number;
/**
* @type integer
* @minimum 0
*/
totalSpansCount?: number;
/**
* @type array,null
*/
uncollapsedSpans?: string[] | null;
}
export enum SpantypesSpanMapperOperationDTO {
move = 'move',
copy = 'copy',
@@ -7013,31 +6770,6 @@ export interface SpantypesPostableSpanMapperGroupDTO {
name: string;
}
export interface SpantypesSpanAggregationDTO {
aggregation?: SpantypesSpanAggregationTypeDTO;
field?: TelemetrytypesTelemetryFieldKeyDTO;
}
export interface SpantypesPostableWaterfallDTO {
/**
* @type array,null
*/
aggregations?: SpantypesSpanAggregationDTO[] | null;
/**
* @type integer
* @minimum 0
*/
limit?: number;
/**
* @type string
*/
selectedSpanId?: string;
/**
* @type array,null
*/
uncollapsedSpans?: string[] | null;
}
export interface SpantypesSpanMapperDTO {
config: SpantypesSpanMapperConfigDTO;
/**
@@ -7146,6 +6878,281 @@ export interface TelemetrytypesGettableFieldValuesDTO {
values: TelemetrytypesTelemetryFieldValuesDTO;
}
export type TracedetailtypesEventDTOAttributeMap = { [key: string]: unknown };
export interface TracedetailtypesEventDTO {
/**
* @type object
*/
attributeMap?: TracedetailtypesEventDTOAttributeMap;
/**
* @type boolean
*/
isError?: boolean;
/**
* @type string
*/
name?: string;
/**
* @type integer
* @minimum 0
*/
timeUnixNano?: number;
}
export type TracedetailtypesGettableWaterfallTraceDTOServiceNameToTotalDurationMapAnyOf =
{ [key: string]: number };
/**
* @nullable
*/
export type TracedetailtypesGettableWaterfallTraceDTOServiceNameToTotalDurationMap =
TracedetailtypesGettableWaterfallTraceDTOServiceNameToTotalDurationMapAnyOf | null;
export enum TracedetailtypesSpanAggregationTypeDTO {
span_count = 'span_count',
execution_time_percentage = 'execution_time_percentage',
duration = 'duration',
}
export type TracedetailtypesSpanAggregationResultDTOValueAnyOf = {
[key: string]: number;
};
/**
* @nullable
*/
export type TracedetailtypesSpanAggregationResultDTOValue =
TracedetailtypesSpanAggregationResultDTOValueAnyOf | null;
export interface TracedetailtypesSpanAggregationResultDTO {
aggregation?: TracedetailtypesSpanAggregationTypeDTO;
field?: TelemetrytypesTelemetryFieldKeyDTO;
/**
* @type object,null
*/
value?: TracedetailtypesSpanAggregationResultDTOValue;
}
export type TracedetailtypesWaterfallSpanDTOAttributesAnyOf = {
[key: string]: unknown;
};
/**
* @nullable
*/
export type TracedetailtypesWaterfallSpanDTOAttributes =
TracedetailtypesWaterfallSpanDTOAttributesAnyOf | null;
export type TracedetailtypesWaterfallSpanDTOResourceAnyOf = {
[key: string]: string;
};
/**
* @nullable
*/
export type TracedetailtypesWaterfallSpanDTOResource =
TracedetailtypesWaterfallSpanDTOResourceAnyOf | null;
export interface TracedetailtypesWaterfallSpanDTO {
/**
* @type object,null
*/
attributes?: TracedetailtypesWaterfallSpanDTOAttributes;
/**
* @type string
*/
db_name?: string;
/**
* @type string
*/
db_operation?: string;
/**
* @type integer
* @minimum 0
*/
duration_nano?: number;
/**
* @type array,null
*/
events?: TracedetailtypesEventDTO[] | null;
/**
* @type string
*/
external_http_method?: string;
/**
* @type string
*/
external_http_url?: string;
/**
* @type integer
* @minimum 0
*/
flags?: number;
/**
* @type boolean
*/
has_children?: boolean;
/**
* @type boolean
*/
has_error?: boolean;
/**
* @type string
*/
http_host?: string;
/**
* @type string
*/
http_method?: string;
/**
* @type string
*/
http_url?: string;
/**
* @type string
*/
is_remote?: string;
/**
* @type string
*/
kind_string?: string;
/**
* @type integer
* @minimum 0
*/
level?: number;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
parent_span_id?: string;
/**
* @type object,null
*/
resource?: TracedetailtypesWaterfallSpanDTOResource;
/**
* @type string
*/
response_status_code?: string;
/**
* @type string
*/
span_id?: string;
/**
* @type integer
*/
status_code?: number;
/**
* @type string
*/
status_code_string?: string;
/**
* @type string
*/
status_message?: string;
/**
* @type integer
* @minimum 0
*/
sub_tree_node_count?: number;
/**
* @type integer
* @minimum 0
*/
time_unix?: number;
/**
* @type string
*/
trace_id?: string;
/**
* @type string
*/
trace_state?: string;
}
export interface TracedetailtypesGettableWaterfallTraceDTO {
/**
* @type array,null
*/
aggregations?: TracedetailtypesSpanAggregationResultDTO[] | null;
/**
* @type integer
* @minimum 0
*/
endTimestampMillis?: number;
/**
* @type boolean
*/
hasMissingSpans?: boolean;
/**
* @type boolean
*/
hasMore?: boolean;
/**
* @type string
*/
rootServiceEntryPoint?: string;
/**
* @type string
*/
rootServiceName?: string;
/**
* @type object,null
*/
serviceNameToTotalDurationMap?: TracedetailtypesGettableWaterfallTraceDTOServiceNameToTotalDurationMap;
/**
* @type array,null
*/
spans?: TracedetailtypesWaterfallSpanDTO[] | null;
/**
* @type integer
* @minimum 0
*/
startTimestampMillis?: number;
/**
* @type integer
* @minimum 0
*/
totalErrorSpansCount?: number;
/**
* @type integer
* @minimum 0
*/
totalSpansCount?: number;
/**
* @type array,null
*/
uncollapsedSpans?: string[] | null;
}
export interface TracedetailtypesSpanAggregationDTO {
aggregation?: TracedetailtypesSpanAggregationTypeDTO;
field?: TelemetrytypesTelemetryFieldKeyDTO;
}
export interface TracedetailtypesPostableWaterfallDTO {
/**
* @type array,null
*/
aggregations?: TracedetailtypesSpanAggregationDTO[] | null;
/**
* @type integer
* @minimum 0
*/
limit?: number;
/**
* @type string
*/
selectedSpanId?: string;
/**
* @type array,null
*/
uncollapsedSpans?: string[] | null;
}
export interface TypesChangePasswordRequestDTO {
/**
* @type string
@@ -9225,18 +9232,7 @@ export type GetWaterfallPathParameters = {
traceID: string;
};
export type GetWaterfall200 = {
data: SpantypesGettableWaterfallTraceDTO;
/**
* @type string
*/
status: string;
};
export type GetWaterfallV4PathParameters = {
traceID: string;
};
export type GetWaterfallV4200 = {
data: SpantypesGettableWaterfallTraceDTO;
data: TracedetailtypesGettableWaterfallTraceDTO;
/**
* @type string
*/

View File

@@ -14,10 +14,8 @@ import type {
import type {
GetWaterfall200,
GetWaterfallPathParameters,
GetWaterfallV4200,
GetWaterfallV4PathParameters,
RenderErrorResponseDTO,
SpantypesPostableWaterfallDTO,
TracedetailtypesPostableWaterfallDTO,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
@@ -29,14 +27,14 @@ import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
*/
export const getWaterfall = (
{ traceID }: GetWaterfallPathParameters,
spantypesPostableWaterfallDTO?: BodyType<SpantypesPostableWaterfallDTO>,
tracedetailtypesPostableWaterfallDTO?: BodyType<TracedetailtypesPostableWaterfallDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetWaterfall200>({
url: `/api/v3/traces/${traceID}/waterfall`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: spantypesPostableWaterfallDTO,
data: tracedetailtypesPostableWaterfallDTO,
signal,
});
};
@@ -50,7 +48,7 @@ export const getGetWaterfallMutationOptions = <
TError,
{
pathParams: GetWaterfallPathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
data?: BodyType<TracedetailtypesPostableWaterfallDTO>;
},
TContext
>;
@@ -59,7 +57,7 @@ export const getGetWaterfallMutationOptions = <
TError,
{
pathParams: GetWaterfallPathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
data?: BodyType<TracedetailtypesPostableWaterfallDTO>;
},
TContext
> => {
@@ -76,7 +74,7 @@ export const getGetWaterfallMutationOptions = <
Awaited<ReturnType<typeof getWaterfall>>,
{
pathParams: GetWaterfallPathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
data?: BodyType<TracedetailtypesPostableWaterfallDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -91,7 +89,7 @@ export type GetWaterfallMutationResult = NonNullable<
Awaited<ReturnType<typeof getWaterfall>>
>;
export type GetWaterfallMutationBody =
| BodyType<SpantypesPostableWaterfallDTO>
| BodyType<TracedetailtypesPostableWaterfallDTO>
| undefined;
export type GetWaterfallMutationError = ErrorType<RenderErrorResponseDTO>;
@@ -107,7 +105,7 @@ export const useGetWaterfall = <
TError,
{
pathParams: GetWaterfallPathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
data?: BodyType<TracedetailtypesPostableWaterfallDTO>;
},
TContext
>;
@@ -116,108 +114,9 @@ export const useGetWaterfall = <
TError,
{
pathParams: GetWaterfallPathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
data?: BodyType<TracedetailtypesPostableWaterfallDTO>;
},
TContext
> => {
return useMutation(getGetWaterfallMutationOptions(options));
};
/**
* Two-step fetch: minimal fields for all spans to build the tree, full fields only for the visible window. Aggregations are not included in the response.
* @summary Get waterfall view for a trace (OOM-safe)
*/
export const getWaterfallV4 = (
{ traceID }: GetWaterfallV4PathParameters,
spantypesPostableWaterfallDTO?: BodyType<SpantypesPostableWaterfallDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetWaterfallV4200>({
url: `/api/v4/traces/${traceID}/waterfall`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: spantypesPostableWaterfallDTO,
signal,
});
};
export const getGetWaterfallV4MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getWaterfallV4>>,
TError,
{
pathParams: GetWaterfallV4PathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof getWaterfallV4>>,
TError,
{
pathParams: GetWaterfallV4PathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
},
TContext
> => {
const mutationKey = ['getWaterfallV4'];
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 getWaterfallV4>>,
{
pathParams: GetWaterfallV4PathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return getWaterfallV4(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type GetWaterfallV4MutationResult = NonNullable<
Awaited<ReturnType<typeof getWaterfallV4>>
>;
export type GetWaterfallV4MutationBody =
| BodyType<SpantypesPostableWaterfallDTO>
| undefined;
export type GetWaterfallV4MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get waterfall view for a trace (OOM-safe)
*/
export const useGetWaterfallV4 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getWaterfallV4>>,
TError,
{
pathParams: GetWaterfallV4PathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof getWaterfallV4>>,
TError,
{
pathParams: GetWaterfallV4PathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
},
TContext
> => {
return useMutation(getGetWaterfallV4MutationOptions(options));
};

View File

@@ -51,6 +51,13 @@
background: var(--l1-background);
}
.progress-container {
.ant-progress-bg {
height: 8px !important;
border-radius: 4px;
}
}
.ant-table-tbody > tr:hover > td {
background: color-mix(in srgb, var(--l1-foreground) 4%, transparent);
}

View File

@@ -9,13 +9,13 @@ import {
Flex,
Input,
InputRef,
Progress,
Space,
Spin,
TableColumnsType,
TableColumnType,
Tooltip,
} from 'antd';
import { Progress } from '@signozhq/ui/progress';
import { Typography } from '@signozhq/ui/typography';
import type { FilterDropdownProps } from 'antd/lib/table/interface';
import logEvent from 'api/common/logEvent';
@@ -59,7 +59,7 @@ function ProgressRender(item: string | number): JSX.Element {
<Progress
percent={percent}
strokeLinecap="butt"
showInfo
size="small"
strokeColor={((): string => {
const cpuPercent = percent;
if (cpuPercent >= 90) {

View File

@@ -137,6 +137,7 @@ function CreateServiceAccountModal(): JSX.Element {
<AuthZTooltip checks={[SACreatePermission]}>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form="create-sa-form"
variant="solid"
color="primary"

View File

@@ -11,6 +11,9 @@ import { GuardAuthZ } from './GuardAuthZ';
describe('GuardAuthZ', () => {
const TestChild = (): ReactElement => <div>Protected Content</div>;
const LoadingFallback = (): ReactElement => <div>Loading...</div>;
const ErrorFallback = (error: Error): ReactElement => (
<div>Error occurred: {error.message}</div>
);
const NoPermissionFallback = (_response: {
requiredPermissionName: BrandedPermission;
}): ReactElement => <div>Access denied</div>;
@@ -87,28 +90,40 @@ describe('GuardAuthZ', () => {
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render children when API error occurs and no fallbackOnError provided (fail open)', async () => {
it('should render fallbackOnError when API error occurs', async () => {
const errorMessage = 'Internal Server Error';
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
return res(ctx.status(500), ctx.json({ error: errorMessage }));
}),
);
render(
<GuardAuthZ relation="read" object="role:*">
<GuardAuthZ relation="read" object="role:*" fallbackOnError={ErrorFallback}>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Protected Content')).toBeInTheDocument();
expect(screen.getByText(/Error occurred:/)).toBeInTheDocument();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render fallbackOnError when API error occurs and fallbackOnError is provided', async () => {
it('should pass error object to fallbackOnError function', async () => {
const errorMessage = 'Network request failed';
let receivedError: Error | null = null;
const errorFallbackWithCapture = (error: Error): ReactElement => {
receivedError = error;
return <div>Captured error: {error.message}</div>;
};
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
return res(ctx.status(500), ctx.json({ error: errorMessage }));
}),
);
@@ -116,14 +131,35 @@ describe('GuardAuthZ', () => {
<GuardAuthZ
relation="read"
object="role:*"
fallbackOnError={<div>Custom error fallback</div>}
fallbackOnError={errorFallbackWithCapture}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Custom error fallback')).toBeInTheDocument();
expect(receivedError).not.toBeNull();
});
expect(receivedError).toBeInstanceOf(Error);
expect(screen.getByText(/Captured error:/)).toBeInTheDocument();
});
it('should render null when error occurs and no fallbackOnError provided', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
const { container } = render(
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(container.firstChild).toBeNull();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();

View File

@@ -12,7 +12,7 @@ export type GuardAuthZProps<R extends AuthZRelation> = {
relation: R;
object: AuthZObject<R>;
fallbackOnLoading?: JSX.Element;
fallbackOnError?: JSX.Element;
fallbackOnError?: (error: Error) => JSX.Element;
fallbackOnNoPermissions?: (response: {
requiredPermissionName: BrandedPermission;
}) => JSX.Element;
@@ -35,7 +35,7 @@ export function GuardAuthZ<R extends AuthZRelation>({
}
if (error) {
return fallbackOnError ?? children;
return fallbackOnError?.(error) ?? null;
}
if (!permissions?.[permission]?.isGranted) {

View File

@@ -4,8 +4,7 @@ import { useSelector } from 'react-redux';
import { matchPath, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Button, Switch } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
@@ -126,8 +125,9 @@ function ShareURLModal(): JSX.Element {
<Info size={14} color={Color.BG_AMBER_600} />
)}
<Switch
value={enableAbsoluteTime}
checked={enableAbsoluteTime}
disabled={!isValidateRelativeTime}
size="small"
onChange={(): void => {
setEnableAbsoluteTime((prev) => !prev);
}}

View File

@@ -5,10 +5,7 @@ import cx from 'classnames';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import {
getBodyDisplayString,
getSanitizedLogBody,
} from 'container/LogDetailedView/utils';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -199,7 +196,7 @@ function ListLogView({
{updatedSelecedFields.some((field) => field.name === 'body') && (
<LogGeneralField
fieldKey="Log"
fieldValue={getBodyDisplayString(logData.body)}
fieldValue={flattenLogData.body}
linesPerRow={linesPerRow}
fontSize={fontSize}
/>

View File

@@ -13,7 +13,7 @@ export function NoAuthBanner(): JSX.Element {
Impersonation mode: authentication is disabled. Anyone with access to this
instance has admin privileges.{' '}
<a
href="https://signoz.io/docs/manage/administrator-guide/configuration/impersonation-mode/"
href="https://signoz.io/docs/manage/administrator-guide/configuration/no-auth-mode/"
target="_blank"
rel="noreferrer"
>

View File

@@ -51,10 +51,6 @@
}
}
}
.duration-input-slider {
padding: 12px 0px;
margin-top: 12px;
}
}
.divider {

View File

@@ -14,8 +14,7 @@ import {
ComboboxList,
ComboboxTrigger,
} from '@signozhq/ui/combobox';
import { Skeleton, Tooltip } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Skeleton, Switch, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
@@ -282,8 +281,9 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
<div className="api-quick-filters-header">
<Typography.Text>Show IP addresses</Typography.Text>
<Switch
size="small"
style={{ marginLeft: 'auto' }}
value={showIP ?? true}
checked={showIP ?? true}
onChange={(checked): void => {
logEvent('API Monitoring: Show IP addresses clicked', {
showIP: checked,

View File

@@ -4,8 +4,7 @@ import type {
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
} from 'antd';
import { Button, Dropdown, Flex, MenuProps } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
@@ -61,7 +60,9 @@ function DynamicColumnTable({
const onToggleHandler =
(index: number, column: ColumnGroupType<any> | ColumnType<any>) =>
(checked: boolean): void => {
(checked: boolean, event: React.MouseEvent<HTMLButtonElement>): void => {
event.stopPropagation();
if (shouldSendAlertsLogEvent) {
logEvent('Alert: Column toggled', {
column: column?.title,
@@ -87,14 +88,10 @@ function DynamicColumnTable({
const items: MenuProps['items'] =
dynamicColumns?.map((column, index) => ({
label: (
<div
className="dynamicColumnsTable-items"
onClick={(e): void => e.stopPropagation()}
role="presentation"
>
<div className="dynamicColumnsTable-items">
<div>{column.title?.toString()}</div>
<Switch
value={columnsData?.findIndex((c) => c.key === column.key) !== -1}
checked={columnsData?.findIndex((c) => c.key === column.key) !== -1}
onChange={onToggleHandler(index, column)}
/>
</div>

View File

@@ -127,6 +127,7 @@ function KeyFormPhase({
>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form={FORM_ID}
variant="solid"
color="primary"

View File

@@ -190,6 +190,7 @@ function EditKeyForm({
>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form={FORM_ID}
variant="solid"
color="primary"

View File

@@ -204,7 +204,7 @@ describe('createGuardedRoute', () => {
).not.toBeInTheDocument();
});
it('should render the component when API error occurs (fail open)', async () => {
it('should render error fallback when API error occurs', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
@@ -230,8 +230,12 @@ describe('createGuardedRoute', () => {
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument();
});
expect(
screen.queryByText('Test Component: test-value'),
).not.toBeInTheDocument();
});
it('should render no permissions fallback when permission is denied', async () => {

View File

@@ -9,11 +9,14 @@ import { parsePermission } from 'hooks/useAuthZ/utils';
import noDataUrl from '@/assets/Icons/no-data.svg';
import ErrorBoundaryFallback from '../../pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import AppLoading from '../AppLoading/AppLoading';
import { GuardAuthZ } from '../GuardAuthZ/GuardAuthZ';
import './createGuardedRoute.styles.scss';
const onErrorFallback = (): JSX.Element => <ErrorBoundaryFallback />;
function OnNoPermissionsFallback(response: {
requiredPermissionName: BrandedPermission;
}): ReactElement {
@@ -60,6 +63,7 @@ export function createGuardedRoute<P extends object, R extends AuthZRelation>(
relation={relation}
object={resolvedObject}
fallbackOnLoading={<AppLoading />}
fallbackOnError={onErrorFallback}
fallbackOnNoPermissions={(response): ReactElement => (
<OnNoPermissionsFallback {...response} />
)}

View File

@@ -11,5 +11,4 @@ export enum FeatureKeys {
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
USE_JSON_BODY = 'use_json_body',
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
USE_DASHBOARD_V2 = 'use_dashboard_v2',
}

View File

@@ -1,64 +0,0 @@
// Collapsed activity summary — one row that hides the underlying
// thinking + tool-call steps. Reuses the same quiet treatment as
// ThinkingStep / ToolCallStep so it sits flush in the assistant bubble.
.activityGroup {
width: 100%;
font-size: 12px;
}
.activityHeader {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
background: transparent;
border: none;
cursor: pointer;
color: var(--l3-foreground);
user-select: none;
transition: color 0.12s ease;
width: 100%;
text-align: left;
&:hover {
color: var(--l1-foreground);
}
}
.sparkleIcon {
flex-shrink: 0;
color: var(--accent-primary);
&.iconPulsing {
animation: activityGroupPulse 1.4s ease-in-out infinite;
}
}
@keyframes activityGroupPulse {
0%,
100% {
opacity: 0.55;
}
50% {
opacity: 1;
}
}
.activitySummary {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 400;
}
.toggleChevron {
flex-shrink: 0;
color: inherit;
}
.activityBody {
display: flex;
flex-direction: column;
padding: 2px 0 4px;
}

View File

@@ -1,148 +0,0 @@
import { useEffect, useRef, useState } from 'react';
import cx from 'classnames';
import { ChevronDown, ChevronRight, Sparkles } from '@signozhq/icons';
import { formatTime } from 'utils/timeUtils';
import { StreamingToolCall } from '../../types';
import ThinkingStep, { ThinkingContent, thinkingLabel } from '../ThinkingStep';
import ToolCallStep, {
getToolDisplayLabel,
ToolCallContent,
} from '../ToolCallStep';
import styles from './ActivityGroup.module.scss';
export type ActivityItem =
| { id: string; kind: 'thinking'; content: string }
| { id: string; kind: 'tool'; toolCall: StreamingToolCall };
interface ActivityGroupProps {
items: ActivityItem[];
/**
* True only for the trailing activity group of an active stream — drives
* the live "Working…" label and the elapsed-time tick (which re-stamps on
* approval/clarification resume so wait time isn't counted).
*/
isLive?: boolean;
}
/**
* Single-item groups get a step-specific summary so the user doesn't see a
* pointless "Worked through 1 step". Multi-item groups roll up into the
* generic "Working… / Worked through N steps" treatment.
*/
function buildSummary(
items: ActivityItem[],
isLive: boolean,
elapsed: number,
): string {
if (items.length === 1) {
const [only] = items;
if (only.kind === 'thinking') {
return thinkingLabel(isLive);
}
return getToolDisplayLabel(only.toolCall);
}
const stepLabel = `${items.length} steps`;
if (!isLive) {
return `Worked through ${stepLabel}`;
}
// Suppress the elapsed token until ≥ 1s — the first tick fires after
// 1s anyway, and showing "0s" or "<1s" briefly adds noise.
return elapsed >= 1000
? `Working… · ${formatTime(elapsed / 1000)} · ${stepLabel}`
: `Working… · ${stepLabel}`;
}
/**
* Single collapsed summary row that hides a run of thinking + tool-call steps.
* Expands to show each underlying step inline. Used for every activity row
* (including single-item ones) so all "what the agent did" rows share a
* consistent ✨-led visual contract.
*/
export default function ActivityGroup({
items,
isLive = false,
}: ActivityGroupProps): JSX.Element {
const [expanded, setExpanded] = useState(false);
// Captures the moment this live phase started. Re-stamped on every
// false→true transition so a stream that pauses on
// approval/clarification and resumes doesn't roll the user's wait time
// into the elapsed counter.
const startedAtRef = useRef<number>(Date.now());
const wasLiveRef = useRef<boolean>(isLive);
const [elapsed, setElapsed] = useState(0);
useEffect(() => {
if (isLive && !wasLiveRef.current) {
startedAtRef.current = Date.now();
setElapsed(0);
}
wasLiveRef.current = isLive;
if (!isLive) {
return undefined;
}
// Tick once per second — the display is integer-second precision, so
// faster ticks would just re-render the bubble for no visible change.
const id = window.setInterval(() => {
setElapsed(Date.now() - startedAtRef.current);
}, 1000);
return (): void => window.clearInterval(id);
}, [isLive]);
const summary = buildSummary(items, isLive, elapsed);
const isSingle = items.length === 1;
const toggle = (): void => setExpanded((v) => !v);
return (
<div className={styles.activityGroup}>
<div className={styles.activityHeader} onClick={toggle}>
<Sparkles
size={12}
className={cx(styles.sparkleIcon, { [styles.iconPulsing]: isLive })}
/>
<span className={styles.activitySummary}>{summary}</span>
{expanded ? (
<ChevronDown size={12} className={styles.toggleChevron} />
) : (
<ChevronRight size={12} className={styles.toggleChevron} />
)}
</div>
{expanded && (
<div className={styles.activityBody}>
{isSingle ? (
// Single-item: the outer chevron already provides disclosure,
// so render the underlying content directly instead of wrapping
// it in a second collapsible step row.
items[0].kind === 'thinking' ? (
<ThinkingContent content={items[0].content} />
) : (
<ToolCallContent toolCall={items[0].toolCall} />
)
) : (
items.map((item, i) => {
// A thinking step is live only while it's the trailing item
// in a trailing live group — once any later event (text or
// tool) arrives, the pass is done.
const isLastItem = i === items.length - 1;
return item.kind === 'thinking' ? (
<ThinkingStep
key={item.id}
content={item.content}
isLive={isLive && isLastItem}
/>
) : (
<ToolCallStep key={item.id} toolCall={item.toolCall} />
);
})
)}
</div>
)}
</div>
);
}

View File

@@ -1,2 +0,0 @@
export { default } from './ActivityGroup';
export type { ActivityItem } from './ActivityGroup';

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React from 'react';
import cx from 'classnames';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
@@ -9,10 +9,11 @@ import '../blocks';
import { useVariant } from '../../VariantContext';
import { Message, MessageBlock } from '../../types';
import ActionsSection from '../ActionsSection';
import ActivityGroup, { ActivityItem } from '../ActivityGroup';
import { RichCodeBlock } from '../blocks';
import { MessageContext } from '../MessageContext';
import MessageFeedback from '../MessageFeedback';
import ThinkingStep from '../ThinkingStep';
import ToolCallStep from '../ToolCallStep';
import UserMessageActions from '../UserMessageActions';
import styles from './MessageBubble.module.scss';
@@ -39,61 +40,38 @@ function SmartPre({ children }: { children?: React.ReactNode }): JSX.Element {
const MD_PLUGINS = [remarkGfm];
const MD_COMPONENTS = { code: RichCodeBlock, pre: SmartPre };
type RenderGroup =
| { kind: 'text'; id: string; content: string }
| { kind: 'activity'; id: string; items: ActivityItem[] };
/**
* Partition message blocks into render groups so consecutive thinking and
* tool_call blocks collapse into a single ActivityGroup row. Text blocks
* stand alone, mirroring the streaming view.
*/
function groupBlocks(blocks: MessageBlock[]): RenderGroup[] {
const groups: RenderGroup[] = [];
blocks.forEach((block, i) => {
if (block.type === 'text') {
groups.push({ kind: 'text', id: `text-${i}`, content: block.content });
return;
}
const item: ActivityItem =
block.type === 'thinking'
? { id: `t-${i}`, kind: 'thinking', content: block.content }
: {
id: `c-${block.toolCallId}`,
kind: 'tool',
// Persisted blocks are always complete.
toolCall: {
toolName: block.toolName,
input: block.toolInput,
result: block.result,
done: true,
displayText: block.displayText,
},
};
const last = groups[groups.length - 1];
if (last?.kind === 'activity') {
last.items.push(item);
} else {
groups.push({ kind: 'activity', id: `a-${i}`, items: [item] });
}
});
return groups;
}
function renderGroup(group: RenderGroup): JSX.Element {
if (group.kind === 'text') {
return (
<ReactMarkdown
key={group.id}
className={styles.markdown}
remarkPlugins={MD_PLUGINS}
components={MD_COMPONENTS}
>
{group.content}
</ReactMarkdown>
);
/** Renders a single MessageBlock by type. */
function renderBlock(block: MessageBlock, index: number): JSX.Element {
switch (block.type) {
case 'thinking':
return <ThinkingStep key={index} content={block.content} />;
case 'tool_call':
// Blocks in a persisted message are always complete — done is always true.
return (
<ToolCallStep
key={index}
toolCall={{
toolName: block.toolName,
input: block.toolInput,
result: block.result,
done: true,
displayText: block.displayText,
}}
/>
);
case 'text':
default:
return (
<ReactMarkdown
key={index}
className={styles.markdown}
remarkPlugins={MD_PLUGINS}
components={MD_COMPONENTS}
>
{block.content}
</ReactMarkdown>
);
}
return <ActivityGroup key={group.id} items={group.items} />;
}
interface MessageBubbleProps {
@@ -112,14 +90,6 @@ export default function MessageBubble({
const isUser = message.role === 'user';
const hasBlocks = !isUser && message.blocks && message.blocks.length > 0;
// Recompute groups only when the blocks array identity changes — store
// updates that don't touch this message's blocks should not re-render the
// underlying ThinkingStep/ToolCallStep children.
const groups = useMemo(
() => (hasBlocks ? groupBlocks(message.blocks!) : []),
[hasBlocks, message.blocks],
);
const messageClass = cx(
styles.message,
isUser ? styles.user : styles.assistant,
@@ -158,7 +128,8 @@ export default function MessageBubble({
<p className={styles.text}>{message.content}</p>
) : hasBlocks ? (
<MessageContext.Provider value={{ messageId: message.id }}>
{groups.map((g) => renderGroup(g))}
{/* eslint-disable-next-line react/no-array-index-key */}
{message.blocks!.map((block, i) => renderBlock(block, i))}
</MessageContext.Provider>
) : (
<MessageContext.Provider value={{ messageId: message.id }}>

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React from 'react';
import cx from 'classnames';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
@@ -10,10 +10,11 @@ import type {
import { useVariant } from '../../VariantContext';
import { StreamingEventItem } from '../../types';
import ActivityGroup, { ActivityItem } from '../ActivityGroup';
import ApprovalCard from '../ApprovalCard';
import { RichCodeBlock } from '../blocks';
import ClarificationForm from '../ClarificationForm';
import ThinkingStep from '../ThinkingStep';
import ToolCallStep from '../ToolCallStep';
import messageStyles from '../MessageBubble/MessageBubble.module.scss';
import styles from './StreamingMessage.module.scss';
@@ -32,59 +33,6 @@ function SmartPre({ children }: { children?: React.ReactNode }): JSX.Element {
const MD_PLUGINS = [remarkGfm];
const MD_COMPONENTS = { code: RichCodeBlock, pre: SmartPre };
type RenderGroup =
| { kind: 'text'; id: string; content: string }
| {
kind: 'activity';
id: string;
items: ActivityItem[];
isTrailing: boolean;
};
/**
* Partition the streaming event timeline into render groups: runs of
* consecutive thinking/tool events fold into a single activity group, text
* events stay standalone. The last group is flagged as trailing so the
* caller can drive a "live" indicator on it.
*
* Invariant relied on by the ActivityGroup elapsed-time timer: once a
* group exists at a given array index, later events only extend its
* `items` — they never shrink the array or re-key existing groups. That
* keeps each ActivityGroup React instance stable across re-renders so the
* timer's `wasLive` → `isLive` re-stamp captures the right transition.
* The id fields below piggyback on that invariant: each event's position in
* `events` is stable, so the derived id stays stable across re-renders.
*/
function groupStreamingEvents(events: StreamingEventItem[]): RenderGroup[] {
const groups: RenderGroup[] = [];
events.forEach((event, i) => {
if (event.kind === 'text') {
groups.push({ kind: 'text', id: `text-${i}`, content: event.content });
return;
}
const item: ActivityItem =
event.kind === 'thinking'
? { id: `t-${i}`, kind: 'thinking', content: event.content }
: { id: `c-${i}`, kind: 'tool', toolCall: event.toolCall };
const last = groups[groups.length - 1];
if (last?.kind === 'activity') {
last.items.push(item);
} else {
groups.push({
kind: 'activity',
id: `a-${i}`,
items: [item],
isTrailing: false,
});
}
});
const last = groups[groups.length - 1];
if (last?.kind === 'activity') {
last.isTrailing = true;
}
return groups;
}
/** Human-readable labels for execution status codes shown before any events arrive. */
const STATUS_LABEL: Record<string, string> = {
queued: 'Queued…',
@@ -131,11 +79,6 @@ export default function StreamingMessage({
[messageStyles.compact]: isCompact,
});
// Recompute groups only when the events array identity changes. The
// streaming reducer pushes new entries into the same array reference
// once per tick, so this naturally invalidates as events arrive.
const groups = useMemo(() => groupStreamingEvents(events), [events]);
return (
<div className={messageClass}>
<div className={messageStyles.bubble}>
@@ -145,28 +88,27 @@ export default function StreamingMessage({
)}
{isEmpty && !statusLabel && <TypingDots />}
{/* Runs of consecutive thinking + tool events collapse into a
single ActivityGroup; text events render inline between
them. The trailing group is "live" while streaming is
active and not blocked on the user. */}
{groups.map((group) => {
if (group.kind === 'text') {
return (
<ReactMarkdown
key={group.id}
className={messageStyles.markdown}
remarkPlugins={MD_PLUGINS}
components={MD_COMPONENTS}
>
{group.content}
</ReactMarkdown>
);
{/* eslint-disable react/no-array-index-key */}
{/* Events rendered in arrival order: text, thinking, and tool calls interleaved */}
{events.map((event, i) => {
if (event.kind === 'tool') {
return <ToolCallStep key={i} toolCall={event.toolCall} />;
}
if (event.kind === 'thinking') {
return <ThinkingStep key={i} content={event.content} />;
}
const groupIsLive = group.isTrailing && !isWaitingOnUser;
return (
<ActivityGroup key={group.id} items={group.items} isLive={groupIsLive} />
<ReactMarkdown
key={i}
className={messageStyles.markdown}
remarkPlugins={MD_PLUGINS}
components={MD_COMPONENTS}
>
{event.content}
</ReactMarkdown>
);
})}
{/* eslint-enable react/no-array-index-key */}
{/* While events are still streaming, append the typing dots so the
user has a clear "more is coming" signal. Hidden when the agent

View File

@@ -5,31 +5,11 @@ import styles from './ThinkingStep.module.scss';
interface ThinkingStepProps {
content: string;
/**
* When false, label reads "Thought for a few seconds" — intentionally
* vague because the API doesn't persist precise timing, so showing
* seconds would be inconsistent between fresh and reloaded threads.
*/
isLive?: boolean;
}
/** Body of a thinking step — extracted so ActivityGroup can render it directly. */
export function ThinkingContent({ content }: { content: string }): JSX.Element {
return (
<div className={styles.body}>
<p className={styles.content}>{content}</p>
</div>
);
}
export function thinkingLabel(isLive: boolean): string {
return isLive ? 'Thinking…' : 'Thought for a few seconds';
}
/** Collapsible thinking row — chevron + label, content in the expanded body. */
export default function ThinkingStep({
content,
isLive = false,
}: ThinkingStepProps): JSX.Element {
const [expanded, setExpanded] = useState(false);
@@ -43,10 +23,14 @@ export default function ThinkingStep({
) : (
<ChevronRight size={12} className={styles.chevron} />
)}
<span className={styles.label}>{thinkingLabel(isLive)}</span>
<span className={styles.label}>Thinking</span>
</div>
{expanded && <ThinkingContent content={content} />}
{expanded && (
<div className={styles.body}>
<p className={styles.content}>{content}</p>
</div>
)}
</div>
);
}

View File

@@ -10,58 +10,24 @@ interface ToolCallStepProps {
toolCall: StreamingToolCall;
}
/**
* Server-supplied `displayText` is the human-friendly title the backend
* wants surfaced. Falls back to a derived label
* ("signoz_get_dashboard" → "Get Dashboard") when missing.
*/
export function getToolDisplayLabel(toolCall: StreamingToolCall): string {
const { toolName, displayText } = toolCall;
if (displayText && displayText.trim().length > 0) {
return displayText;
}
return toolName
.replace(/^[a-z]+_/, '') // strip prefix like "signoz_"
.replace(/_/g, ' ')
.replace(/\b\w/g, (c) => c.toUpperCase());
}
/** Body of a tool-call step — extracted so ActivityGroup can render it directly. */
export function ToolCallContent({
toolCall,
}: {
toolCall: StreamingToolCall;
}): JSX.Element {
const { toolName, input, result, done } = toolCall;
return (
<div className={styles.body}>
<div className={styles.section}>
<span className={styles.sectionLabel}>Tool</span>
<span className={styles.toolName}>{toolName}</span>
</div>
<div className={styles.section}>
<span className={styles.sectionLabel}>Input</span>
<pre className={styles.json}>{JSON.stringify(input, null, 2)}</pre>
</div>
{done && result !== undefined && (
<div className={styles.section}>
<span className={styles.sectionLabel}>Output</span>
<pre className={styles.json}>
{typeof result === 'string' ? result : JSON.stringify(result, null, 2)}
</pre>
</div>
)}
</div>
);
}
/** Collapsible tool-call row — chevron + label, in/out detail in the body. */
export default function ToolCallStep({
toolCall,
}: ToolCallStepProps): JSX.Element {
const [expanded, setExpanded] = useState(false);
const { done } = toolCall;
const label = getToolDisplayLabel(toolCall);
const { toolName, input, result, done, displayText } = toolCall;
// Prefer the server-supplied `displayText` from `ToolCallEventDTO` —
// it's the human-friendly title the backend wants surfaced. Fall back
// to a derived label ("signoz_get_dashboard" → "Get Dashboard") when
// the field is empty / null / missing.
const label =
displayText && displayText.trim().length > 0
? displayText
: toolName
.replace(/^[a-z]+_/, '') // strip prefix like "signoz_"
.replace(/_/g, ' ')
.replace(/\b\w/g, (c) => c.toUpperCase());
const toggle = (): void => setExpanded((v) => !v);
@@ -78,7 +44,26 @@ export default function ToolCallStep({
<span className={styles.label}>{label}</span>
</div>
{expanded && <ToolCallContent toolCall={toolCall} />}
{expanded && (
<div className={styles.body}>
<div className={styles.section}>
<span className={styles.sectionLabel}>Tool</span>
<span className={styles.toolName}>{toolName}</span>
</div>
<div className={styles.section}>
<span className={styles.sectionLabel}>Input</span>
<pre className={styles.json}>{JSON.stringify(input, null, 2)}</pre>
</div>
{done && result !== undefined && (
<div className={styles.section}>
<span className={styles.sectionLabel}>Output</span>
<pre className={styles.json}>
{typeof result === 'string' ? result : JSON.stringify(result, null, 2)}
</pre>
</div>
)}
</div>
)}
</div>
);
}

View File

@@ -45,10 +45,6 @@
.contributors-row {
height: 80px;
}
.top-contributors-progress {
--progress-background: transparent;
}
&__content {
.ant-table {
&-cell {

View File

@@ -1,7 +1,6 @@
import { HTMLAttributes } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Table, TableColumnsType as ColumnsType } from 'antd';
import { Progress } from '@signozhq/ui/progress';
import { Progress, Table, TableColumnsType as ColumnsType } from 'antd';
import logEvent from 'api/common/logEvent';
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
import AlertLabels from 'pages/AlertDetails/AlertHeader/AlertLabels/AlertLabels';
@@ -52,8 +51,8 @@ function TopContributorsRows({
<Progress
percent={(count / totalCurrentTriggers) * 100}
showInfo={false}
trailColor="rgba(255, 255, 255, 0)"
strokeColor={Color.BG_ROBIN_500}
className="top-contributors-progress"
/>
</ConditionalAlertPopover>
),

View File

@@ -141,9 +141,12 @@
.progress-container {
width: 158px;
.ant-progress {
margin: 0;
span {
font-weight: 600;
.ant-progress-text {
font-weight: 600;
}
}
}

View File

@@ -1,8 +1,7 @@
import { useMemo, useState } from 'react';
import { QueryFunctionContext, useQueries, useQuery } from 'react-query';
import { Spin, Table, Tooltip } from 'antd';
import { Spin, Switch, Table, Tooltip } from 'antd';
import { Info, Loader } from '@signozhq/icons';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import { getQueryRangeV5 } from 'api/v5/queryRange/getQueryRange';
import { MetricRangePayloadV5, ScalarData } from 'api/v5/v5';
@@ -171,7 +170,11 @@ function TopErrors({
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Switch value={showStatusCodeErrors} onChange={setShowStatusCodeErrors} />
<Switch
checked={showStatusCodeErrors}
onChange={setShowStatusCodeErrors}
size="small"
/>
<span style={{ color: 'white', fontSize: '14px' }}>
Status Message Exists
</span>

View File

@@ -1,8 +1,7 @@
import { useMemo } from 'react';
import { useQueries } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Skeleton, Tooltip } from 'antd';
import { Progress } from '@signozhq/ui/progress';
import { Progress, Skeleton, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
@@ -137,11 +136,12 @@ function DomainMetrics({
<Tooltip title={formattedDomainMetricsData.errorRate}>
{formattedDomainMetricsData.errorRate !== '-' ? (
<Progress
status="active"
percent={Number(
Number(formattedDomainMetricsData.errorRate).toFixed(2),
)}
strokeLinecap="butt"
showInfo
size="small"
strokeColor={((): string => {
const errorRatePercent = Number(
Number(formattedDomainMetricsData.errorRate).toFixed(2),

View File

@@ -1,8 +1,7 @@
import { useMemo } from 'react';
import { UseQueryResult } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Skeleton, Tooltip } from 'antd';
import { Progress } from '@signozhq/ui/progress';
import { Progress, Skeleton, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import {
getDisplayValue,
@@ -81,9 +80,10 @@ function EndPointMetrics({
<Tooltip title={metricsData?.errorRate}>
{metricsData?.errorRate !== '-' ? (
<Progress
status="active"
percent={Number(Number(metricsData?.errorRate ?? 0).toFixed(2))}
strokeLinecap="butt"
showInfo
size="small"
strokeColor={((): string => {
const errorRatePercent = Number(
Number(metricsData?.errorRate ?? 0).toFixed(2),

View File

@@ -1,7 +1,6 @@
import { ReactNode } from 'react';
import { Color } from '@signozhq/design-tokens';
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
import { Progress } from '@signozhq/ui/progress';
import { Progress, TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/utils';
import {
FiltersType,
@@ -258,9 +257,10 @@ export const columnsConfig: ColumnType<APIDomainsRowData>[] = [
errorRate === 'n/a' || errorRate === '-' ? 0 : errorRate;
return (
<Progress
status="active"
percent={Number((errorRateValue as number).toFixed(2))}
strokeLinecap="butt"
showInfo
size="small"
strokeColor={((): string => {
const errorRatePercent = Number((errorRateValue as number).toFixed(2));
if (errorRatePercent >= 90) {
@@ -1022,13 +1022,14 @@ export const getEndPointsColumnsConfig = (
className: `column`,
render: (errorRate: number | string): React.ReactNode => (
<Progress
status="active"
percent={Number(
(
(errorRate === 'n/a' || errorRate === '-' ? 0 : errorRate) as number
).toFixed(1),
)}
strokeLinecap="butt"
showInfo
size="small"
strokeColor={((): string => {
const errorRatePercent = Number((errorRate as number).toFixed(1));
if (errorRatePercent >= 90) {
@@ -2513,9 +2514,10 @@ export const dependentServicesColumns: ColumnType<DependentServicesData>[] = [
render: (errorPercentage: number | string): React.ReactNode =>
errorPercentage !== '-' ? (
<Progress
status="active"
percent={Number((errorPercentage as number).toFixed(2))}
strokeLinecap="butt"
showInfo
size="small"
strokeColor={((): string => {
const errorPercentagePercent = Number(
(errorPercentage as number).toFixed(2),
@@ -3020,13 +3022,14 @@ export const getAllEndpointsWidgetData = (
),
F1: (errorRate: any): ReactNode => (
<Progress
status="active"
percent={Number(
(
(errorRate === 'n/a' || errorRate === '-' ? 0 : errorRate) as number
).toFixed(2),
)}
strokeLinecap="butt"
showInfo
size="small"
strokeColor={((): string => {
const errorRatePercent = Number(
(

View File

@@ -1,5 +1,4 @@
import { Button, Flex, SelectProps } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Button, Flex, SelectProps, Switch } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import type { BaseOptionType, DefaultOptionType } from 'antd/es/select';
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
@@ -420,8 +419,8 @@ export function RoutingPolicyBanner({
</Typography.Text>
<div className="routing-policies-info-banner-right">
<Switch
value={notificationSettings.routingPolicies}
testId="routing-policies-switch"
checked={notificationSettings.routingPolicies}
data-testid="routing-policies-switch"
onChange={(value): void => {
setNotificationSettings({
type: 'SET_ROUTING_POLICIES',

View File

@@ -1,6 +1,5 @@
import { useEffect, useState } from 'react';
import { Tooltip } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Switch, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Info } from '@signozhq/icons';
@@ -50,7 +49,7 @@ function AdvancedOptionItem({
>
{input}
</div>
<Switch onChange={handleOnToggle} value={showInput} />
<Switch onChange={handleOnToggle} checked={showInput} />
</div>
</div>
);

View File

@@ -91,6 +91,7 @@ function ChartPreview({
const renderQBChartPreview = (): JSX.Element => (
<ChartPreviewComponent
headline={headline}
name=""
query={stagedQuery}
selectedInterval={globalSelectedInterval}
alertDef={alertDef}
@@ -106,6 +107,7 @@ function ChartPreview({
const renderPromAndChQueryChartPreview = (): JSX.Element => (
<ChartPreviewComponent
headline={headline}
name="Chart Preview"
query={stagedQuery}
alertDef={alertDef}
selectedInterval={globalSelectedInterval}

View File

@@ -17,6 +17,7 @@ import { CreateAlertProvider } from '../../context';
import ChartPreview from '../ChartPreview/ChartPreview';
const REQUESTS_PER_SEC = 'requests/sec';
const CHART_PREVIEW_NAME = 'Chart Preview';
const QUERY_TYPE_TEST_ID = 'query-type';
const GRAPH_TYPE_TEST_ID = 'graph-type';
const CHART_PREVIEW_COMPONENT_TEST_ID = 'chart-preview-component';
@@ -33,6 +34,7 @@ jest.mock(
return (
<div data-testid={CHART_PREVIEW_COMPONENT_TEST_ID}>
<div data-testid="headline">{props.headline}</div>
<div data-testid="name">{props.name}</div>
<div data-testid={QUERY_TYPE_TEST_ID}>{props.query?.queryType}</div>
<div data-testid="selected-interval">
{props.selectedInterval?.startTime}
@@ -173,6 +175,12 @@ describe('ChartPreview', () => {
);
});
it('renders QueryBuilder chart preview with empty name when query type is QUERY_BUILDER', () => {
renderChartPreview();
expect(screen.getByTestId('name')).toHaveTextContent('');
});
it('renders QueryBuilder chart preview with correct props', () => {
renderChartPreview();
@@ -183,6 +191,7 @@ describe('ChartPreview', () => {
expect(screen.getByTestId(GRAPH_TYPE_TEST_ID)).toHaveTextContent(
PANEL_TYPES.TIME_SERIES,
);
expect(screen.getByTestId('name')).toHaveTextContent('');
expect(screen.getByTestId('headline')).toBeInTheDocument();
expect(screen.getByTestId('selected-interval')).toBeInTheDocument();
});
@@ -205,6 +214,7 @@ describe('ChartPreview', () => {
expect(
screen.getByTestId(CHART_PREVIEW_COMPONENT_TEST_ID),
).toBeInTheDocument();
expect(screen.getByTestId('name')).toHaveTextContent(CHART_PREVIEW_NAME);
expect(screen.getByTestId(QUERY_TYPE_TEST_ID)).toHaveTextContent(
EQueryType.PROM,
);
@@ -228,6 +238,7 @@ describe('ChartPreview', () => {
expect(
screen.getByTestId(CHART_PREVIEW_COMPONENT_TEST_ID),
).toBeInTheDocument();
expect(screen.getByTestId('name')).toHaveTextContent(CHART_PREVIEW_NAME);
expect(screen.getByTestId(QUERY_TYPE_TEST_ID)).toHaveTextContent(
EQueryType.CLICKHOUSE,
);

View File

@@ -5,8 +5,7 @@ import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { orange } from '@ant-design/colors';
import { Color } from '@signozhq/design-tokens';
import { Button, Collapse, Input, Select, Tag } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Button, Collapse, Input, Select, Switch, Tag } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import cx from 'classnames';
@@ -764,7 +763,7 @@ function VariableItem({
</Typography>
</LabelContainer>
<Switch
value={variableMultiSelect}
checked={variableMultiSelect}
onChange={(e): void => {
setVariableMultiSelect(e);
if (!e) {
@@ -781,7 +780,7 @@ function VariableItem({
</Typography>
</LabelContainer>
<Switch
value={variableShowALLOption}
checked={variableShowALLOption}
onChange={(e): void => setVariableShowALLOption(e)}
/>
</VariableItemRow>

View File

@@ -1,147 +0,0 @@
import { renderHook } from '@testing-library/react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { usePanelContextMenu } from '../usePanelContextMenu';
// The hook composes `useCoordinates` (popover state) and `useGraphContextMenu`
// (menu items). We mock both so the test focuses on the `enableDrillDown` gate
// rather than the implementation of the menu wiring itself.
const onClickMock = jest.fn();
jest.mock('periscope/components/ContextMenu', () => ({
useCoordinates: (): unknown => ({
coordinates: null,
popoverPosition: null,
clickedData: null,
onClose: jest.fn(),
subMenu: null,
onClick: onClickMock,
setSubMenu: jest.fn(),
}),
}));
jest.mock('container/QueryTable/Drilldown/useGraphContextMenu', () => ({
__esModule: true,
default: (): { menuItemsConfig: { header: string; items: string } } => ({
menuItemsConfig: { header: 'menu-header', items: 'menu-items' },
}),
}));
jest.mock('container/QueryTable/Drilldown/drilldownUtils', () => ({
getUplotClickData: jest.fn(() => ({
coord: { x: 1, y: 2 },
record: { queryName: 'A', filters: [] },
label: 'lbl',
seriesColor: '#abc',
})),
}));
jest.mock('container/PanelWrapper/utils', () => ({
isApmMetric: jest.fn(() => false),
getTimeRangeFromStepInterval: jest.fn(() => ({ start: 0, end: 0 })),
}));
const mockWidget = { id: 'w-1', query: {} } as unknown as Widgets;
const mockQueryResponse = {
data: undefined,
isLoading: false,
} as unknown as UseQueryResult<
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
describe('usePanelContextMenu', () => {
beforeEach(() => {
onClickMock.mockClear();
});
it('returns empty menuItemsConfig when enableDrillDown is false', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
enableDrillDown: false,
}),
);
expect(result.current.menuItemsConfig).toStrictEqual({});
});
it('returns wired menuItemsConfig when enableDrillDown is true', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
enableDrillDown: true,
}),
);
expect(result.current.menuItemsConfig).toStrictEqual({
header: 'menu-header',
items: 'menu-items',
});
});
it('clickHandlerWithContextMenu is a no-op when enableDrillDown is false', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
enableDrillDown: false,
}),
);
result.current.clickHandlerWithContextMenu(
100, // xValue
200, // yValue
0, // mouseX
0, // mouseY
{ serviceName: 'svc' }, // metric
{ queryName: 'A', inFocusOrNot: true }, // queryData
10, // absoluteMouseX
20, // absoluteMouseY
{}, // axesData
{ seriesIndex: 0, seriesName: 'A', value: 1, color: '#abc' }, // focusedSeries
);
expect(onClickMock).not.toHaveBeenCalled();
});
it('clickHandlerWithContextMenu opens popover when enableDrillDown is true', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
enableDrillDown: true,
}),
);
result.current.clickHandlerWithContextMenu(
100,
200,
0,
0,
{ serviceName: 'svc' },
{ queryName: 'A', inFocusOrNot: true },
10,
20,
{},
{ seriesIndex: 0, seriesName: 'A', value: 1, color: '#abc' },
);
expect(onClickMock).toHaveBeenCalledTimes(1);
});
it('defaults to disabled when enableDrillDown is not provided', () => {
const { result } = renderHook(() =>
usePanelContextMenu({
widget: mockWidget,
queryResponse: mockQueryResponse,
}),
);
expect(result.current.menuItemsConfig).toStrictEqual({});
});
});

View File

@@ -21,13 +21,11 @@ interface UseTimeSeriesContextMenuParams {
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
enableDrillDown?: boolean;
}
export const usePanelContextMenu = ({
widget,
queryResponse,
enableDrillDown = false,
}: UseTimeSeriesContextMenuParams): {
coordinates: { x: number; y: number } | null;
popoverPosition: PopoverPosition | null;
@@ -63,9 +61,6 @@ export const usePanelContextMenu = ({
const clickHandlerWithContextMenu = useCallback(
(...args: any[]) => {
if (!enableDrillDown) {
return;
}
const [
xValue,
_yvalue,
@@ -117,14 +112,14 @@ export const usePanelContextMenu = ({
});
}
},
[enableDrillDown, onClick, queryResponse],
[onClick, queryResponse],
);
return {
coordinates,
popoverPosition,
onClose,
menuItemsConfig: enableDrillDown ? menuItemsConfig : {},
menuItemsConfig,
clickHandlerWithContextMenu,
};
};

View File

@@ -17,11 +17,10 @@ import { getTimeRange } from 'utils/getTimeRange';
import BarChart from '../../charts/BarChart/BarChart';
import ChartManager from '../../components/ChartManager/ChartManager';
import { usePanelContextMenu } from '../../hooks/usePanelContextMenu';
import { prepareBarPanelConfig } from './utils';
import { prepareBarPanelConfig, prepareBarPanelData } from './utils';
import '../Panel.styles.scss';
import TooltipFooter from '../components/TooltipFooter';
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
function BarPanel(props: PanelWrapperProps): JSX.Element {
const {
@@ -32,7 +31,6 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
isFullViewMode,
onToggleModelHandler,
groupByPerQuery,
enableDrillDown = false,
} = props;
const uPlotRef = useRef<uPlot | null>(null);
const graphRef = useRef<HTMLDivElement>(null);
@@ -63,7 +61,6 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
} = usePanelContextMenu({
widget,
queryResponse,
enableDrillDown,
});
const config = useMemo(() => {
@@ -100,7 +97,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
if (!queryResponse?.data?.payload) {
return [];
}
return prepareChartData(queryResponse?.data?.payload);
return prepareBarPanelData(queryResponse?.data?.payload);
}, [queryResponse?.data?.payload]);
const layoutChildren = useMemo(() => {

View File

@@ -11,10 +11,21 @@ import { get } from 'lodash-es';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { AlignedData } from 'uplot';
import { PanelMode } from '../types';
import { fillMissingXAxisTimestamps, getXAxisTimestamps } from '../utils';
import { buildBaseConfig } from '../utils/baseConfigBuilder';
export function prepareBarPanelData(
apiResponse: MetricRangePayloadProps,
): AlignedData {
const seriesList = apiResponse?.data?.result || [];
const timestampArr = getXAxisTimestamps(seriesList);
const yAxisValuesArr = fillMissingXAxisTimestamps(timestampArr, seriesList);
return [timestampArr, ...yAxisValuesArr];
}
export function prepareBarPanelConfig({
widget,
isDarkMode,

View File

@@ -17,11 +17,10 @@ import { useTimezone } from 'providers/Timezone';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
import { prepareUPlotConfig } from '../TimeSeriesPanel/utils';
import { prepareChartData, prepareUPlotConfig } from '../TimeSeriesPanel/utils';
import '../Panel.styles.scss';
import TooltipFooter from '../components/TooltipFooter';
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
const {
@@ -32,7 +31,6 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
isFullViewMode,
onToggleModelHandler,
groupByPerQuery,
enableDrillDown = false,
} = props;
const graphRef = useRef<HTMLDivElement>(null);
const [minTimeScale, setMinTimeScale] = useState<number>();
@@ -62,7 +60,6 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
} = usePanelContextMenu({
widget,
queryResponse,
enableDrillDown,
});
const chartData = useMemo(() => {

View File

@@ -6,8 +6,7 @@ import {
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { PanelMode } from '../../types';
import { prepareUPlotConfig } from '../utils';
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
import { prepareChartData, prepareUPlotConfig } from '../utils';
jest.mock(
'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils',

View File

@@ -1,6 +1,10 @@
import { ExecStats } from 'api/v5/v5';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
fillMissingXAxisTimestamps,
getXAxisTimestamps,
} from 'container/DashboardContainer/visualization/panels/utils';
import { getLegend } from 'lib/dashboard/getQueryResults';
import getLabelName from 'lib/getLabelName';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
@@ -11,15 +15,42 @@ import {
LineStyle,
} from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { hasSingleVisiblePoint } from 'lib/uPlotV2/utils/dataUtils';
import { isInvalidPlotValue } from 'lib/uPlotV2/utils/dataUtils';
import get from 'lodash-es/get';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryData } from 'types/api/widgets/getQuery';
import { PanelMode } from '../types';
import { buildBaseConfig } from '../utils/baseConfigBuilder';
export const prepareChartData = (
apiResponse: MetricRangePayloadProps,
): uPlot.AlignedData => {
const seriesList = apiResponse?.data?.result || [];
const timestampArr = getXAxisTimestamps(seriesList);
const yAxisValuesArr = fillMissingXAxisTimestamps(timestampArr, seriesList);
return [timestampArr, ...yAxisValuesArr];
};
function hasSingleVisiblePointForSeries(series: QueryData): boolean {
const rawValues = series.values ?? [];
let validPointCount = 0;
for (const [, rawValue] of rawValues) {
if (!isInvalidPlotValue(rawValue)) {
validPointCount += 1;
if (validPointCount > 1) {
return false;
}
}
}
return true;
}
export const prepareUPlotConfig = ({
widget,
isDarkMode,
@@ -76,7 +107,7 @@ export const prepareUPlotConfig = ({
}
apiResponse.data.result.forEach((series) => {
const hasSingleValidPoint = hasSingleVisiblePoint(series.values);
const hasSingleValidPoint = hasSingleVisiblePointForSeries(series);
const baseLabelName = getLabelName(
series.metric,
series.queryName || '', // query

View File

@@ -1,7 +1,6 @@
import { Dispatch, ReactElement, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { Form, FormInstance, Input, Select } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Form, FormInstance, Input, Select, Switch } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import type { Store } from 'antd/lib/form/interface';
import ROUTES from 'constants/routes';
@@ -83,8 +82,8 @@ function FormAlertChannels({
name="send_resolved"
>
<Switch
defaultValue={initialValue?.send_resolved}
testId="field-send-resolved-checkbox"
defaultChecked={initialValue?.send_resolved}
data-testid="field-send-resolved-checkbox"
onChange={(value): void => {
setSelectedConfig((state) => ({
...state,

View File

@@ -2,8 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { Plus } from '@signozhq/icons';
import { Button, Flex, Form, Select, Tooltip } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Button, Flex, Form, Select, Switch, Tooltip } from 'antd';
import getAll from 'api/channels/getAll';
import logEvent from 'api/common/logEvent';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
@@ -199,10 +198,10 @@ function BasicInfo({
placement="right"
>
<Switch
value={shouldBroadCastToAllChannels}
checked={shouldBroadCastToAllChannels}
onChange={handleBroadcastToAllChannels}
disabled={noChannels || !!isLoading}
testId="alert-broadcast-to-all-channels"
data-testid="alert-broadcast-to-all-channels"
/>
</Tooltip>
</FormItemMedium>

View File

@@ -1,118 +0,0 @@
import { useMemo } from 'react';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import uPlot from 'uplot';
import {
AlertChartPanelType,
buildAlertChartConfig,
buildChartId,
} from './utils';
// Panel types that render through the UPlotConfigBuilder pipeline.
// To support a new modern-chart panel type, add an entry here and extend
// `AlertChartPanelType` / `buildAlertChartConfig` to handle its series setup.
const SUPPORTED_CHARTS: Record<
AlertChartPanelType,
typeof TimeSeries | typeof BarChart
> = {
[PANEL_TYPES.TIME_SERIES]: TimeSeries,
[PANEL_TYPES.BAR]: BarChart,
};
const isSupportedPanelType = (
panelType: PANEL_TYPES,
): panelType is AlertChartPanelType => panelType in SUPPORTED_CHARTS;
export interface ChartContentProps {
panelType: PANEL_TYPES;
alertId?: string;
query: Query;
apiResponse?: MetricRangePayloadProps;
data: uPlot.AlignedData;
thresholds: ThresholdProps[];
yAxisUnit: string;
legendPosition: LegendPosition;
isDarkMode: boolean;
timezone: Timezone;
width: number;
height: number;
minTimeScale?: number;
maxTimeScale?: number;
onDragSelect: (start: number, end: number) => void;
}
export default function ChartContent({
panelType,
alertId,
query,
thresholds,
apiResponse,
data,
yAxisUnit,
isDarkMode,
timezone,
minTimeScale,
maxTimeScale,
onDragSelect,
width,
height,
legendPosition,
}: ChartContentProps): JSX.Element | null {
const supported = isSupportedPanelType(panelType);
const config = useMemo(
() =>
buildAlertChartConfig({
id: buildChartId(alertId),
panelType: panelType as AlertChartPanelType,
query,
thresholds,
apiResponse,
yAxisUnit,
isDarkMode,
timezone,
minTimeScale,
maxTimeScale,
onDragSelect,
}),
[
alertId,
panelType,
query,
thresholds,
apiResponse,
yAxisUnit,
isDarkMode,
timezone,
minTimeScale,
maxTimeScale,
onDragSelect,
],
);
if (!supported) {
return null;
}
const Component = SUPPORTED_CHARTS[panelType];
return (
<Component
config={config}
data={data}
width={width}
height={height}
legendConfig={{ position: legendPosition }}
canPinTooltip
yAxisUnit={yAxisUnit}
timezone={timezone}
/>
);
}

View File

@@ -15,6 +15,8 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
import { INITIAL_CRITICAL_THRESHOLD } from 'container/CreateAlertV2/context/constants';
import { Threshold } from 'container/CreateAlertV2/context/types';
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
@@ -30,7 +32,8 @@ import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { isEmpty } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { useTimezone } from 'providers/Timezone';
@@ -38,27 +41,24 @@ import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Warning } from 'types/api';
import { AlertDef } from 'types/api/alerts/def';
import { LegendPosition } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import uPlot from 'uplot';
import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
import { AlertDetectionTypes } from '..';
import ChartContent from './ChartContent';
import { ChartContainer } from './styles';
import { getThresholds } from './utils';
import './ChartPreview.styles.scss';
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
// Height reserved for the `.chart-preview-header` strip rendered above the chart.
const CHART_PREVIEW_HEADER_HEIGHT = 48;
const CHART_PREVIEW_CONTAINER_PADDING = 16;
export interface ChartPreviewProps {
name: string;
query: Query | null;
graphType?: PANEL_TYPES;
selectedTime?: timePreferenceType;
@@ -77,6 +77,7 @@ export interface ChartPreviewProps {
// eslint-disable-next-line sonarjs/cognitive-complexity
function ChartPreview({
name,
query,
graphType = PANEL_TYPES.TIME_SERIES,
selectedTime = 'GLOBAL_TIME',
@@ -112,6 +113,14 @@ function ChartPreview({
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const [graphVisibility, setGraphVisibility] = useState<boolean[]>([]);
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
const { currentQuery } = useQueryBuilder();
const {
@@ -210,6 +219,18 @@ function ChartPreview({
setMaxTimeScale(endTime);
}, [maxTime, minTime, globalSelectedInterval, queryResponse, setQueryStatus]);
// Initialize graph visibility from localStorage
useEffect(() => {
if (queryResponse?.data?.payload?.data?.result) {
const { graphVisibilityStates: localStoredVisibilityState } =
getLocalStorageGraphVisibilityState({
apiResponse: queryResponse.data.payload.data.result,
name: 'alert-chart-preview',
});
setGraphVisibility(localStoredVisibilityState);
}
}, [queryResponse?.data?.payload?.data?.result]);
if (queryResponse.data && graphType === PANEL_TYPES.BAR) {
const sortedSeriesData = getSortedSeriesData(
queryResponse.data?.payload.data.result,
@@ -267,17 +288,62 @@ function ChartPreview({
return LegendPosition.RIGHT;
}, [queryResponse?.data?.payload?.data?.result?.length, showSideLegend]);
const resolvedThresholds = useMemo(
() => getThresholds(thresholds, t, optionName, yAxisUnit),
[thresholds, t, optionName, yAxisUnit],
const options = useMemo(
() =>
getUPlotChartOptions({
id: 'alert_legend_widget',
yAxisUnit,
apiResponse: queryResponse?.data?.payload,
dimensions: {
height: containerDimensions?.height ? containerDimensions.height - 48 : 0,
width: containerDimensions?.width,
},
minTimeScale,
maxTimeScale,
isDarkMode,
onDragSelect,
thresholds: getThresholds(thresholds, t, optionName, yAxisUnit),
softMax: null,
softMin: null,
panelType: graphType,
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
currentQuery,
query: query || currentQuery,
graphsVisibilityStates: graphVisibility,
setGraphsVisibilityStates: setGraphVisibility,
enhancedLegend: true,
legendPosition,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
}),
[
yAxisUnit,
queryResponse?.data?.payload,
containerDimensions,
minTimeScale,
maxTimeScale,
isDarkMode,
onDragSelect,
thresholds,
t,
optionName,
graphType,
timezone.value,
currentQuery,
query,
graphVisibility,
legendPosition,
],
);
const chartData = useMemo(() => {
if (!queryResponse?.data?.payload) {
return [];
}
return prepareChartData(queryResponse?.data?.payload);
}, [queryResponse?.data?.payload]);
const chartData = getUPlotChartData(queryResponse?.data?.payload);
const hasResultData = !!queryResponse?.data?.payload?.data?.result?.length;
@@ -295,14 +361,6 @@ function ChartPreview({
?.active || false;
const isWarning = !isEmpty(queryResponse.data?.warning);
const chartWidth = containerDimensions?.width
? containerDimensions.width - CHART_PREVIEW_CONTAINER_PADDING
: 0;
const chartHeight = containerDimensions?.height
? containerDimensions.height - CHART_PREVIEW_HEADER_HEIGHT
: 0;
return (
<div className="alert-chart-container" ref={graphRef}>
<ChartContainer>
@@ -326,22 +384,16 @@ function ChartPreview({
)}
{chartDataAvailable && !isAnomalyDetectionAlert && (
<ChartContent
<GridPanelSwitch
options={options}
panelType={graphType}
alertId={alertDef?.id}
query={query || currentQuery}
apiResponse={queryResponse.data?.payload}
data={chartData}
thresholds={resolvedThresholds}
name={name || 'Chart Preview'}
panelData={
queryResponse.data?.payload?.data?.newResult?.data?.result || []
}
query={query || initialQueriesMap.metrics}
yAxisUnit={yAxisUnit}
legendPosition={legendPosition}
isDarkMode={isDarkMode}
timezone={timezone}
width={chartWidth}
height={chartHeight}
minTimeScale={minTimeScale}
maxTimeScale={maxTimeScale}
onDragSelect={onDragSelect}
/>
)}

View File

@@ -1,10 +1,6 @@
import { Color } from '@signozhq/design-tokens';
import { ExecStats } from 'api/v5/v5';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { Threshold } from 'container/CreateAlertV2/context/types';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import {
BooleanFormats,
@@ -15,20 +11,6 @@ import {
TimeFormats,
} from 'container/NewWidget/RightContainer/types';
import { TFunction } from 'i18next';
import { getLegend } from 'lib/dashboard/getQueryResults';
import getLabelName from 'lib/getLabelName';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import {
DrawStyle,
FillMode,
LineInterpolation,
LineStyle,
} from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { hasSingleVisiblePoint } from 'lib/uPlotV2/utils/dataUtils';
import { get } from 'lodash-es';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import {
dataFormatConfig,
@@ -38,8 +20,6 @@ import {
timeUnitsConfig,
} from './config';
const CHART_ID_PREFIX = 'alert_legend_widget';
export function covertIntoDataFormats({
value,
sourceUnit,
@@ -162,110 +142,3 @@ export const getThresholds = (
});
return thresholdsToReturn;
};
export type AlertChartPanelType = PANEL_TYPES.TIME_SERIES | PANEL_TYPES.BAR;
export interface BuildAlertChartConfigParams {
id: string;
panelType: AlertChartPanelType;
query: Query;
thresholds: ThresholdProps[];
apiResponse?: MetricRangePayloadProps;
yAxisUnit?: string;
isDarkMode: boolean;
timezone: Timezone;
minTimeScale?: number;
maxTimeScale?: number;
onDragSelect: (startTime: number, endTime: number) => void;
onClick?: OnClickPluginOpts['onClick'];
}
export const buildAlertChartConfig = ({
id,
panelType,
query,
thresholds,
apiResponse,
yAxisUnit,
isDarkMode,
timezone,
minTimeScale,
maxTimeScale,
onDragSelect,
onClick,
}: BuildAlertChartConfigParams): UPlotConfigBuilder => {
const stepIntervals: ExecStats['stepIntervals'] = get(
apiResponse,
'data.newResult.meta.stepIntervals',
{},
);
const stepIntervalValues = Object.values(stepIntervals);
const minStepInterval = stepIntervalValues.length
? Math.min(...stepIntervalValues)
: undefined;
const builder = buildBaseConfig({
id,
panelType,
panelMode: PanelMode.DASHBOARD_VIEW,
thresholds,
apiResponse,
yAxisUnit,
isDarkMode,
timezone,
minTimeScale,
maxTimeScale,
stepInterval: minStepInterval,
onDragSelect,
onClick,
});
const seriesList = apiResponse?.data?.result;
if (!seriesList?.length) {
return builder;
}
const isBar = panelType === PANEL_TYPES.BAR;
seriesList.forEach((series) => {
const baseLabelName = getLabelName(
series.metric,
series.queryName || '',
series.legend || '',
);
const label = query ? getLegend(series, query, baseLabelName) : baseLabelName;
if (isBar) {
builder.addSeries({
scaleKey: 'y',
drawStyle: DrawStyle.Bar,
label,
colorMapping: {},
isDarkMode,
stepInterval: get(stepIntervals, series.queryName, undefined),
});
return;
}
const hasSingleValidPoint = hasSingleVisiblePoint(series.values);
builder.addSeries({
scaleKey: 'y',
drawStyle: hasSingleValidPoint ? DrawStyle.Points : DrawStyle.Line,
label,
colorMapping: {},
spanGaps: true,
lineStyle: LineStyle.Solid,
lineInterpolation: LineInterpolation.Spline,
showPoints: hasSingleValidPoint,
pointSize: 5,
fillMode: FillMode.None,
isDarkMode,
metric: series.metric,
});
});
return builder;
};
export const buildChartId = (id?: string): string =>
id ? `${CHART_ID_PREFIX}_${id}` : CHART_ID_PREFIX;

View File

@@ -719,6 +719,7 @@ function FormAlertRules({
panelType={panelType || PANEL_TYPES.TIME_SERIES}
/>
}
name=""
query={stagedQuery}
selectedInterval={globalSelectedInterval}
alertDef={alertDef}
@@ -738,6 +739,7 @@ function FormAlertRules({
panelType={panelType || PANEL_TYPES.TIME_SERIES}
/>
}
name="Chart Preview"
query={stagedQuery}
alertDef={alertDef}
selectedInterval={globalSelectedInterval}

View File

@@ -292,8 +292,6 @@ function FullView({
return <Spinner height="100%" size="large" tip="Loading..." />;
}
const showEditBtn = editWidget && dashboardEditView;
return (
<div className="full-view-container">
<OverlayScrollbar>
@@ -308,7 +306,7 @@ function FullView({
Reset Query
</Button>
)}
{showEditBtn && (
{editWidget && (
<Button
className="switch-edit-btn"
disabled={response.isFetching || response.isLoading}

View File

@@ -39,5 +39,7 @@
width: 100% !important;
--progress-width: 100%;
.ant-progress-steps-outer {
width: 100% !important;
}
}

View File

@@ -1,4 +1,4 @@
import { Progress } from '@signozhq/ui/progress';
import { Progress } from 'antd';
import { ChecklistItem } from '../HomeChecklist/HomeChecklist';
@@ -15,7 +15,9 @@ function StepsProgress({
const totalChecklistItems = checklistItems.length;
const progress = (completedChecklistItems.length / totalChecklistItems) * 100;
const progress = Math.round(
(completedChecklistItems.length / totalChecklistItems) * 100,
);
return (
<div className="steps-progress-container">

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { Color } from '@signozhq/design-tokens';
import { Tag } from 'antd';
import { Progress } from '@signozhq/ui/progress';
import { Progress, Tag } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import {
getHostLists,
@@ -80,8 +79,8 @@ export const hostDetailsMetadataConfig: K8sDetailsMetadataConfig<HostData>[] = [
render: (value): React.ReactNode => (
<Progress
percent={Number(Number(value).toFixed(1))}
size="small"
strokeColor={getProgressColor(Number(value))}
showInfo
/>
),
},
@@ -91,8 +90,8 @@ export const hostDetailsMetadataConfig: K8sDetailsMetadataConfig<HostData>[] = [
render: (value): React.ReactNode => (
<Progress
percent={Number(Number(value).toFixed(1))}
size="small"
strokeColor={getMemoryProgressColor(Number(value))}
showInfo
/>
),
},

View File

@@ -60,6 +60,11 @@
& > div {
width: 100%;
}
:global(.ant-progress-bg) {
height: 8px !important;
border-radius: 4px;
}
}
.progressBar {

View File

@@ -103,8 +103,12 @@
.progress-container {
width: 158px;
span {
font-weight: 600;
.ant-progress {
margin: 0;
.ant-progress-text {
font-weight: 600;
}
}
}
@@ -288,6 +292,10 @@
}
.progress-container {
.ant-progress-bg {
height: 8px !important;
border-radius: 4px;
}
}
.ant-table-tbody > tr:hover > td {

View File

@@ -1,4 +1,4 @@
import { Progress } from '@signozhq/ui/progress';
import { Progress } from 'antd';
import TanStackTable from 'components/TanStackTableView';
import {
getMemoryProgressColor,
@@ -53,6 +53,7 @@ export function EntityProgressBar({
<Progress
percent={percentage}
strokeLinecap="butt"
size="small"
status="normal"
strokeColor={getStrokeColor(type, value)}
className={styles.progressBar}

View File

@@ -15,13 +15,13 @@ import {
Modal,
Row,
Select,
Switch,
Table,
TablePaginationConfig,
TableProps as AntDTableProps,
Tag,
Tooltip,
} from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import type { NotificationInstance } from 'antd/es/notification/interface';
import type { CollapseProps } from 'antd/lib';
@@ -1180,7 +1180,8 @@ function MultiIngestionSettings(): JSX.Element {
<div className="limit-enable-disable-toggle">
<Form.Item name="enableDailyLimit">
<Switch
value={activeSignal?.config?.day?.enabled}
size="small"
checked={activeSignal?.config?.day?.enabled}
onChange={(value): void => {
setActiveSignal((prev) =>
prev
@@ -1269,7 +1270,8 @@ function MultiIngestionSettings(): JSX.Element {
<div className="limit-enable-disable-toggle">
<Form.Item name="enableSecondLimit">
<Switch
value={activeSignal?.config?.second?.enabled}
size="small"
checked={activeSignal?.config?.second?.enabled}
onChange={(value): void => {
setActiveSignal((prev) =>
prev

View File

@@ -21,11 +21,11 @@ import {
Modal,
Popover,
Skeleton,
Switch,
Table,
Tag,
Tooltip,
} from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import type { TableProps } from 'antd/lib';
import getLocalStorageKey from 'api/browser/localstorage/get';
@@ -965,7 +965,8 @@ function DashboardsList(): JSX.Element {
<div className="connection-line" />
<div className="right">
<Switch
value
size="small"
checked
disabled
onChange={(check): void =>
setVisibleColumns((prev) => ({
@@ -984,8 +985,9 @@ function DashboardsList(): JSX.Element {
<div className="connection-line" />
<div className="right">
<Switch
size="small"
disabled
value
checked
onChange={(check): void =>
setVisibleColumns((prev) => ({
...prev,
@@ -1003,7 +1005,8 @@ function DashboardsList(): JSX.Element {
<div className="connection-line" />
<div className="right">
<Switch
value={visibleColumns.updatedAt}
size="small"
checked={visibleColumns.updatedAt}
onChange={(check): void =>
setVisibleColumns((prev) => ({
...prev,
@@ -1021,7 +1024,8 @@ function DashboardsList(): JSX.Element {
<div className="connection-line" />
<div className="right">
<Switch
value={visibleColumns.updatedBy}
size="small"
checked={visibleColumns.updatedBy}
onChange={(check): void =>
setVisibleColumns((prev) => ({
...prev,

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Switch } from '@signozhq/ui/switch';
import { Switch } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
@@ -228,8 +228,9 @@ function LiveLogsContainer({
<div className="live-logs-frequency-chart-view-controller">
<Typography>Frequency chart</Typography>
<Switch
value={showLiveLogsFrequencyChart}
defaultValue
size="small"
checked={showLiveLogsFrequencyChart}
defaultChecked
onChange={handleToggleFrequencyChart}
/>
</div>

View File

@@ -1,7 +1,7 @@
import { useMemo, useState } from 'react';
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Switch } from '@signozhq/ui/switch';
import { Switch } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -81,7 +81,7 @@ function JSONView({ logData }: JSONViewProps): JSX.Element {
<div className="log-switch">
<div className="wrap-word-switch">
<Typography.Text>Wrap text</Typography.Text>
<Switch value={isWrapWord} onChange={handleWrapWord} />
<Switch checked={isWrapWord} onChange={handleWrapWord} size="small" />
</div>
</div>
</div>

View File

@@ -2,8 +2,7 @@ import { ReactNode, useState } from 'react';
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { Switch } from '@signozhq/ui/switch';
import { Collapse, Divider, Input, Tag } from 'antd';
import { Collapse, Divider, Input, Switch, Tag } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
@@ -137,7 +136,7 @@ function Overview({
<div className="log-switch">
<div className="wrap-word-switch">
<Typography.Text>Wrap text</Typography.Text>
<Switch value={isWrapWord} onChange={handleWrapWord} />
<Switch checked={isWrapWord} onChange={handleWrapWord} size="small" />
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { Switch } from '@signozhq/ui/switch';
import { Switch } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
@@ -69,8 +69,9 @@ function LogsActionsContainer({
<div className="frequency-chart-view-controller">
<Typography>Frequency chart</Typography>
<Switch
value={showFrequencyChart}
defaultValue
size="small"
checked={showFrequencyChart}
defaultChecked
onChange={handleToggleFrequencyChart}
/>
</div>

View File

@@ -30,12 +30,7 @@ import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import {
GraphTitle,
MENU_ITEMS,
SERVICE_CHART_ID,
SERVICE_DETAIL_DRILLDOWN_ENABLED,
} from '../constant';
import { GraphTitle, MENU_ITEMS, SERVICE_CHART_ID } from '../constant';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { Card, GraphContainer, Row } from '../styles';
import { Button } from './styles';
@@ -211,7 +206,6 @@ function DBCall(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
@@ -250,7 +244,6 @@ function DBCall(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>

View File

@@ -32,12 +32,7 @@ import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import {
GraphTitle,
legend,
MENU_ITEMS,
SERVICE_DETAIL_DRILLDOWN_ENABLED,
} from '../constant';
import { GraphTitle, legend, MENU_ITEMS } from '../constant';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { Card, GraphContainer, Row } from '../styles';
import GraphControlsPanel from './Overview/GraphControlsPanel/GraphControlsPanel';
@@ -284,7 +279,6 @@ function External(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
@@ -328,7 +322,6 @@ function External(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
@@ -373,7 +366,6 @@ function External(): JSX.Element {
}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
@@ -417,7 +409,6 @@ function External(): JSX.Element {
}}
onDragSelect={onDragSelect}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>

View File

@@ -15,7 +15,6 @@ import DisplayThreshold from 'container/GridCardLayout/WidgetHeader/DisplayThres
import {
GraphTitle,
SERVICE_CHART_ID,
SERVICE_DETAIL_DRILLDOWN_ENABLED,
} from 'container/MetricsApplication/constant';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
@@ -106,7 +105,6 @@ function ApDexMetrics({
threshold={threshold}
isQueryEnabled={isQueryEnabled}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
);
}

View File

@@ -8,7 +8,6 @@ import Graph from 'container/GridCardLayout/GridCard';
import {
GraphTitle,
SERVICE_CHART_ID,
SERVICE_DETAIL_DRILLDOWN_ENABLED,
} from 'container/MetricsApplication/constant';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { latency } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
@@ -139,7 +138,6 @@ function ServiceOverview({
onClickHandler={handleGraphClick('Service')}
isQueryEnabled={isQueryEnabled}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
)}
</GraphContainer>

View File

@@ -4,7 +4,6 @@ import axios from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ENTITY_VERSION_V4 } from 'constants/app';
import Graph from 'container/GridCardLayout/GridCard';
import { SERVICE_DETAIL_DRILLDOWN_ENABLED } from 'container/MetricsApplication/constant';
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -44,7 +43,6 @@ function TopLevelOperation({
onDragSelect={onDragSelect}
isQueryEnabled={!topLevelOperationsIsLoading}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
)}
</GraphContainer>

View File

@@ -5,11 +5,11 @@ import { useParams } from 'react-router-dom';
import { Search } from '@signozhq/icons';
import {
InputRef,
Switch,
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
Tooltip,
} from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import { ResizeTable } from 'components/ResizeTable';
import TextToolTip from 'components/TextToolTip';
@@ -212,7 +212,11 @@ function TopOperationsTable({
/>
</div>
<div className="top-operation__entry-point">
<Switch value={isEntryPoint} onChange={onEntryPointToggle} />
<Switch
checked={isEntryPoint}
onChange={onEntryPointToggle}
size="small"
/>
<span className="top-operation__entry-point-label">Entrypoint Spans</span>
<TextToolTip
text={entryPointSpanInfo.text}

View File

@@ -25,8 +25,6 @@ export const OPERATION_LEGENDS = ['Operations'];
export const MENU_ITEMS = [MenuItemKeys.View, MenuItemKeys.CreateAlerts];
export const SERVICE_DETAIL_DRILLDOWN_ENABLED = true;
export enum FORMULA {
ERROR_PERCENTAGE = 'A*100/B',
DATABASE_CALLS_AVG_DURATION = 'A/B',

View File

@@ -2,8 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useSearchParams } from 'react-router-dom-v5-compat';
import * as Sentry from '@sentry/react';
import { Tooltip } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Switch, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import WarningPopover from 'components/WarningPopover/WarningPopover';
@@ -359,9 +358,10 @@ function Explorer(): JSX.Element {
title={oneChartPerQueryDisabledTooltip}
>
<Switch
value={showOneChartPerQuery}
checked={showOneChartPerQuery}
onChange={handleToggleShowOneChartPerQuery}
disabled={disableOneChartPerQuery || splitedQueries.length <= 1}
size="small"
/>
</Tooltip>
</div>

View File

@@ -2,8 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Color } from '@signozhq/design-tokens';
import { Button, Skeleton } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Button, Skeleton, Switch } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import Uplot from 'components/Uplot';
@@ -207,7 +206,7 @@ function GraphView({
</Button.Group>
<div className="view-toggle-button">
<Switch
value={viewType === 'graph'}
checked={viewType === 'graph'}
onChange={(checked): void => {
const newViewType = checked ? 'graph' : 'table';
setViewType(newViewType);

View File

@@ -142,6 +142,13 @@
}
}
.progress-container {
.ant-progress-bg {
height: 8px !important;
border-radius: 4px;
}
}
.ant-table-tbody > tr:hover > td {
background: color-mix(in srgb, var(--l1-foreground) 4%, transparent);
}

View File

@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Switch } from '@signozhq/ui/switch';
import { Switch } from 'antd';
import logEvent from 'api/common/logEvent';
import { Delete } from '@signozhq/icons';
import { useTimezone } from 'providers/Timezone';
@@ -49,10 +49,10 @@ function TimezoneAdaptation(): JSX.Element {
<div className="timezone-adaption__header">
<h2 className="timezone-adaption__title">Adapt to my timezone</h2>
<Switch
value={isAdaptationEnabled}
checked={isAdaptationEnabled}
onChange={handleSwitchChange}
style={getSwitchStyles()}
testId="timezone-adaptation-switch"
data-testid="timezone-adaptation-switch"
/>
</div>

View File

@@ -127,12 +127,6 @@
flex-direction: column;
gap: 8px;
padding-bottom: 16px;
.password-error-text {
font-size: var(--font-size-xs);
color: var(--bg-cherry-400);
margin-top: 2px;
}
}
.ant-color-picker-trigger {

View File

@@ -1,27 +1,25 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Input, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import {
updateMyPassword,
useUpdateMyUserV2,
} from 'api/generated/services/users';
import { toast } from '@signozhq/ui/sonner';
import { useNotifications } from 'hooks/useNotifications';
import { Check, FileTerminal, Mail, User } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { ErrorV2Resp } from 'types/api';
import { AxiosError } from 'axios';
import '../MySettings.styles.scss';
import './UserInfo.styles.scss';
function UserInfo(): JSX.Element {
const { user, org, updateUser } = useAppContext();
const { t } = useTranslation(['routes', 'settings', 'common']);
const { showErrorModal } = useErrorModal();
const { notifications } = useNotifications();
const { mutateAsync: updateMyUser } = useUpdateMyUserV2();
const [currentPassword, setCurrentPassword] = useState<string>('');
@@ -49,8 +47,6 @@ function UserInfo(): JSX.Element {
const hideResetPasswordModal = (): void => {
setIsResetPasswordModalOpen(false);
setCurrentPassword('');
setUpdatePassword('');
};
const onChangePasswordClickHandler = async (): Promise<void> => {
@@ -61,29 +57,27 @@ function UserInfo(): JSX.Element {
newPassword: updatePassword,
oldPassword: currentPassword,
});
toast.success('Password updated successfully');
notifications.success({
message: t('success', {
ns: 'common',
}),
});
hideResetPasswordModal();
setIsLoading(false);
} catch (error) {
setIsLoading(false);
try {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
} catch (apiError) {
showErrorModal(apiError as APIError);
}
notifications.error({
message: (error as APIError).error.error.code,
description: (error as APIError).error.error.message,
});
}
};
const passwordsMatch =
currentPassword.length > 0 &&
updatePassword.length > 0 &&
currentPassword === updatePassword;
const isResetPasswordDisabled =
isLoading ||
currentPassword.length === 0 ||
updatePassword.length === 0 ||
passwordsMatch;
currentPassword === updatePassword;
const onSaveHandler = async (): Promise<void> => {
void logEvent('Account Settings: Name Updated', {
@@ -100,7 +94,11 @@ function UserInfo(): JSX.Element {
setIsLoading(true);
await updateMyUser({ data: { displayName: changedName } });
toast.success('Name updated successfully');
notifications.success({
message: t('success', {
ns: 'common',
}),
});
updateUser({
...user,
displayName: changedName,
@@ -108,11 +106,10 @@ function UserInfo(): JSX.Element {
setIsLoading(false);
hideUpdateNameModal();
} catch (error) {
try {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
} catch (apiError) {
showErrorModal(apiError as APIError);
}
notifications.error({
message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(),
});
}
setIsLoading(false);
};
@@ -169,7 +166,7 @@ function UserInfo(): JSX.Element {
type="primary"
icon={<Check size={16} />}
onClick={onSaveHandler}
loading={isLoading}
disabled={isLoading}
data-testid="update-name-btn"
>
Update name
@@ -181,11 +178,7 @@ function UserInfo(): JSX.Element {
<Input
placeholder="e.g. John Doe"
value={changedName}
disabled={isLoading}
onChange={(e): void => setChangedName(e.target.value)}
onPressEnter={(): void => {
void onSaveHandler();
}}
/>
</div>
</Modal>
@@ -195,7 +188,6 @@ function UserInfo(): JSX.Element {
title={<span className="title">Reset password</span>}
open={isResetPasswordModalOpen}
closable
destroyOnClose
onCancel={hideResetPasswordModal}
footer={[
<Button
@@ -205,8 +197,7 @@ function UserInfo(): JSX.Element {
}`}
icon={<Check size={16} />}
onClick={onChangePasswordClickHandler}
loading={isLoading}
disabled={isResetPasswordDisabled}
disabled={isLoading || isResetPasswordDisabled}
data-testid="reset-password-btn"
>
Reset password
@@ -227,11 +218,6 @@ function UserInfo(): JSX.Element {
type="password"
autoComplete="off"
visibilityToggle
onPressEnter={(): void => {
if (!isResetPasswordDisabled) {
void onChangePasswordClickHandler();
}
}}
/>
</div>
@@ -249,18 +235,7 @@ function UserInfo(): JSX.Element {
type="password"
autoComplete="off"
visibilityToggle={false}
status={passwordsMatch ? 'error' : ''}
onPressEnter={(): void => {
if (!isResetPasswordDisabled) {
void onChangePasswordClickHandler();
}
}}
/>
{passwordsMatch && (
<span className="password-error-text">
New password must be different from current password
</span>
)}
</div>
</div>
</Modal>

View File

@@ -8,23 +8,11 @@ import {
waitFor,
within,
} from 'tests/test-utils';
import APIError from 'types/api/error';
import { toast } from '@signozhq/ui/sonner';
const toggleThemeFunction = jest.fn();
const logEventFunction = jest.fn();
const copyToClipboardFn = jest.fn();
const editUserFn = jest.fn();
const updateMyPasswordFn = jest.fn();
const showErrorModalFn = jest.fn();
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: {
success: jest.fn(),
error: jest.fn(),
},
}));
jest.mock('react-use', () => ({
__esModule: true,
@@ -36,21 +24,12 @@ jest.mock('react-use', () => ({
jest.mock('api/generated/services/users', () => ({
...jest.requireActual('api/generated/services/users'),
updateMyPassword: (...args: unknown[]): Promise<unknown> =>
updateMyPasswordFn(...args),
useUpdateMyUserV2: jest.fn(() => ({
mutateAsync: (...args: unknown[]): Promise<unknown> => editUserFn(...args),
isLoading: false,
})),
}));
jest.mock('providers/ErrorModalProvider', () => ({
...jest.requireActual('providers/ErrorModalProvider'),
useErrorModal: jest.fn(() => ({
showErrorModal: showErrorModalFn,
})),
}));
jest.mock('hooks/useDarkMode', () => ({
__esModule: true,
useIsDarkMode: jest.fn(() => true),
@@ -86,12 +65,12 @@ const NEW_PASSWORD_TEST_ID = 'new-password-textbox';
const UPDATE_NAME_BUTTON_TEST_ID = 'update-name-btn';
const RESET_PASSWORD_BUTTON_TEST_ID = 'reset-password-btn';
const UPDATE_NAME_BUTTON_TEXT = 'Update name';
const PASSWORD_VALIDATION_MESSAGE_TEST_ID = 'password-validation-message';
describe('MySettings Flows', () => {
beforeEach(() => {
jest.clearAllMocks();
editUserFn.mockResolvedValue({});
updateMyPasswordFn.mockResolvedValue({});
render(<MySettingsContainer />);
});
@@ -173,7 +152,9 @@ describe('MySettings Flows', () => {
fireEvent.click(modalUpdateNameButton);
await waitFor(() =>
expect(toast.success).toHaveBeenCalledWith('Name updated successfully'),
expect(successNotification).toHaveBeenCalledWith({
message: 'success',
}),
);
});
});
@@ -200,131 +181,22 @@ describe('MySettings Flows', () => {
expect(screen.getByTestId(NEW_PASSWORD_TEST_ID)).toBeInTheDocument();
});
it('Should show inline error when new password matches current password', async () => {
it('Should display validation error if password is less than 8 characters', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
const currentPasswordTextbox = screen.getByTestId(CURRENT_PASSWORD_TEST_ID);
act(() => {
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
target: { value: 'samePassword1' },
});
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'samePassword1' },
});
fireEvent.change(currentPasswordTextbox, { target: { value: '123' } });
});
expect(
screen.getByText('New password must be different from current password'),
).toBeInTheDocument();
expect(screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID)).toBeDisabled();
});
it('Should hide inline error when passwords are changed to be different', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
act(() => {
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
target: { value: 'samePassword1' },
});
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'samePassword1' },
});
});
act(() => {
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'differentPassword1' },
});
});
expect(
screen.queryByText('New password must be different from current password'),
).not.toBeInTheDocument();
});
it('Should show error modal when password reset API returns an error', async () => {
updateMyPasswordFn.mockRejectedValue(
new Error('Current password is incorrect'),
);
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
act(() => {
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
target: { value: 'oldPassword1' },
});
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'newPassword1' },
});
});
fireEvent.click(screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID));
await waitFor(() => {
expect(showErrorModalFn).toHaveBeenCalledWith(expect.any(APIError));
});
});
it('Should show success toast and close modal on successful password reset', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
act(() => {
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
target: { value: 'oldPassword1' },
});
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'newPassword1' },
});
});
fireEvent.click(screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID));
await waitFor(() => {
expect(toast.success).toHaveBeenCalledWith('Password updated successfully');
expect(
screen.queryByTestId(CURRENT_PASSWORD_TEST_ID),
).not.toBeInTheDocument();
});
});
it('Should clear password fields when modal is cancelled', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
act(() => {
fireEvent.change(screen.getByTestId(CURRENT_PASSWORD_TEST_ID), {
target: { value: 'somePassword' },
});
fireEvent.change(screen.getByTestId(NEW_PASSWORD_TEST_ID), {
target: { value: 'otherPassword' },
});
});
expect(screen.getByTestId(CURRENT_PASSWORD_TEST_ID)).toHaveValue(
'somePassword',
);
// Close the modal
const closeButton = document.querySelector(
'.reset-password-modal .ant-modal-close',
) as HTMLElement;
fireEvent.click(closeButton);
// Reopen the modal
await waitFor(() => {
expect(
screen.queryByTestId(CURRENT_PASSWORD_TEST_ID),
).not.toBeInTheDocument();
});
fireEvent.click(screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT)[0]);
await waitFor(() => {
expect(screen.getByTestId(CURRENT_PASSWORD_TEST_ID)).toHaveValue('');
expect(screen.getByTestId(NEW_PASSWORD_TEST_ID)).toHaveValue('');
// Use getByTestId for the validation message (if present in your modal/component)
if (screen.queryByTestId(PASSWORD_VALIDATION_MESSAGE_TEST_ID)) {
expect(
screen.getByTestId(PASSWORD_VALIDATION_MESSAGE_TEST_ID),
).toBeInTheDocument();
}
});
});

View File

@@ -1,7 +1,6 @@
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { Radio, RadioChangeEvent, Tag } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Radio, RadioChangeEvent, Switch, Tag } from 'antd';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import updateUserPreference from 'api/v1/user/preferences/name/update';
@@ -219,10 +218,10 @@ function MySettings(): JSX.Element {
<div className="user-preference-section-content-item-title-action">
Keep the primary sidebar always open{' '}
<Switch
value={sideNavPinned}
checked={sideNavPinned}
onChange={handleSideNavPinnedChange}
disabled={isUpdatingUserPreference}
testId="side-nav-pinned-switch"
loading={isUpdatingUserPreference}
data-testid="side-nav-pinned-switch"
/>
</div>

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