Compare commits

..

10 Commits

Author SHA1 Message Date
Nikhil Soni
1064defd60 chore: update openapi specs 2026-05-29 12:26:42 +05:30
Nikhil Soni
f9222c9930 chore: extract out flamegraph building logic for easier review 2026-05-29 11:45:48 +05:30
Nikhil Soni
511dd0add8 chore: ignore nil assigment lint error 2026-05-29 11:41:19 +05:30
Nikhil Soni
4ef8500b28 chore: move exported methods to the top 2026-05-28 00:26:26 +05:30
Nikhil Soni
168b2eaa9c feat: query full spans for smaller traces 2026-05-27 00:12:38 +05:30
Nikhil Soni
6b613f18a3 feat: add api and module for flamegraph v3 2026-05-26 20:04:20 +05:30
Nikhil Soni
1b0447181d feat: add method to enrich selected spans 2026-05-26 20:03:47 +05:30
Nikhil Soni
20edff4771 feat: add config for flamegraph 2026-05-26 19:21:33 +05:30
Nikhil Soni
2048ef3d2f chore: remove limit from request payload
It's a new api so doesn't need to be backward compatible
2026-05-26 19:06:48 +05:30
Nikhil Soni
53c551359e feat: add types for flamegraph v3 in module structure 2026-05-26 18:56:35 +05:30
556 changed files with 8846 additions and 17610 deletions

View File

@@ -58,6 +58,8 @@ jobs:
run: |
mkdir -p frontend
echo 'CI=1' > frontend/.env
echo 'VITE_INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' >> frontend/.env
echo 'VITE_SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> frontend/.env
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> frontend/.env
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> frontend/.env
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> frontend/.env

View File

@@ -24,6 +24,8 @@ jobs:
- name: dotenv-frontend
working-directory: frontend
run: |
echo 'VITE_INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > .env
echo 'VITE_SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> .env
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> .env
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> .env
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> .env

View File

@@ -33,7 +33,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/retention"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/app"
@@ -101,8 +100,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, authtypes.NewRegistry()), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing, tagModule tag.Module) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, tagModule)
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)
},
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return noopgateway.NewProviderFactory()

View File

@@ -50,7 +50,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/retention"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
@@ -134,8 +133,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
}
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, authtypes.NewRegistry()), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing, tagModule)
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)
},
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return httpgateway.NewProviderFactory(licensing)

View File

@@ -68,12 +68,6 @@ web:
appcues:
# Whether to enable Appcues in web.
enabled: true
sentry:
# Whether to enable Sentry in web.
enabled: true
pylon:
# Whether to enable Pylon in web.
enabled: true
##################### Cache #####################
cache:
@@ -440,6 +434,17 @@ tracedetail:
max_depth_to_auto_expand: 5
# Threshold below which all spans are returned without windowing.
max_limit_to_select_all_spans: 10000
flamegraph:
# Maximum number of BFS depth levels included in a windowed response.
max_selected_levels: 50
# Maximum spans per level before sampling is applied.
max_spans_per_level: 100
# Number of highest-latency spans always included when sampling a level.
sampling_top_latency_count: 5
# Number of timestamp buckets used for uniform sampling within a level.
sampling_bucket_count: 50
# Threshold below which all spans are returned without windowing or sampling.
select_all_spans_limit: 100000
##################### Authz #################################
authz:
@@ -451,12 +456,7 @@ authz:
##################### Meter Reporter #####################
meterreporter:
# The interval between collection ticks. Minimum 10m, maximum 24h.
# The interval between collection ticks. Minimum 5m.
interval: 6h
# Whether to backfill sealed days from the license creation day.
backfill: true
# Random jitter applied to the first collect and to every subsequent cycle.
# The first collect fires at a random time in [0, jitter); each cycle then takes
# interval - random(0, jitter). Must be between 10m and interval. Defaults to
# min(interval, 2h) when unset.
jitter: 2h

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.126.1
image: signoz/signoz:v0.125.1
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.126.1
image: signoz/signoz:v0.125.1
ports:
- "8080:8080" # signoz port
volumes:

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.126.1}
image: signoz/signoz:${VERSION:-v0.125.1}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.126.1}
image: signoz/signoz:${VERSION:-v0.125.1}
container_name: signoz
ports:
- "8080:8080" # signoz port

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,7 @@
{
"required": [
"posthog",
"appcues",
"sentry",
"pylon"
"appcues"
],
"additionalProperties": false,
"definitions": {
@@ -30,30 +28,6 @@
}
},
"type": "object"
},
"Pylon": {
"required": [
"enabled"
],
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
}
},
"type": "object"
},
"Sentry": {
"required": [
"enabled"
],
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
}
},
"type": "object"
}
},
"properties": {
@@ -62,12 +36,6 @@
},
"posthog": {
"$ref": "#/definitions/Posthog"
},
"pylon": {
"$ref": "#/definitions/Pylon"
},
"sentry": {
"$ref": "#/definitions/Sentry"
}
},
"type": "object"

View File

@@ -94,19 +94,17 @@ func newProvider(
func (provider *Provider) Start(ctx context.Context) error {
close(provider.healthyC)
startDelay := provider.config.NewJitter()
provider.collect(ctx)
timer := time.NewTimer(startDelay)
defer timer.Stop()
ticker := time.NewTicker(provider.config.Interval)
defer ticker.Stop()
for {
select {
case <-provider.stopC:
return nil
case <-timer.C:
case <-ticker.C:
provider.collect(ctx)
next := provider.config.Interval - provider.config.NewJitter()
timer.Reset(next)
}
}
}
@@ -259,7 +257,6 @@ func (provider *Provider) report(ctx context.Context, orgID valuer.UUID, license
collectedReadings, err := collector.Collect(ctx, orgID, license, window)
if err != nil {
provider.metrics.collections.Add(ctx, 1, metric.WithAttributes(meterAttr, errors.TypeAttr(err)))
provider.settings.Logger().ErrorContext(ctx, "meter collector failed", errors.Attr(err), slog.String("org_id", orgID.StringValue()), slog.String("meter", collector.Name().String()))
continue
}

View File

@@ -11,7 +11,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/types"
@@ -31,9 +30,9 @@ type module struct {
licensing licensing.Licensing
}
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard")
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser, tagModule)
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser)
return &module{
pkgDashboardModule: pkgDashboardModule,
@@ -213,14 +212,6 @@ func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy s
return module.pkgDashboardModule.Create(ctx, orgID, createdBy, creator, source, data)
}
func (module *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, source dashboardtypes.Source, postable dashboardtypes.PostableDashboardV2) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.CreateV2(ctx, orgID, createdBy, creator, source, postable)
}
func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.GetV2(ctx, orgID, id)
}
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Get(ctx, orgID, id)
}

View File

@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"log/slog"
"net/url"
"sync"
"time"
@@ -48,14 +47,13 @@ func NewAnomalyRule(
p *ruletypes.PostableRule,
querier querier.Querier,
logger *slog.Logger,
externalURL *url.URL,
opts ...baserules.RuleOption,
) (*AnomalyRule, error) {
logger.Info("creating new AnomalyRule", slog.String("rule.id", id))
opts = append(opts, baserules.WithLogger(logger))
baseRule, err := baserules.NewBaseRule(id, orgID, p, externalURL, opts...)
baseRule, err := baserules.NewBaseRule(id, orgID, p, opts...)
if err != nil {
return nil, err
}

View File

@@ -2,7 +2,6 @@ package rules
import (
"context"
"net/url"
"testing"
"time"
@@ -121,7 +120,6 @@ func TestAnomalyRule_NoData_AlertOnAbsent(t *testing.T) {
&postableRule,
nil,
logger,
mustParseURL(t, "http://localhost:8000"),
)
require.NoError(t, err)
@@ -249,8 +247,7 @@ func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
},
}
externalURL := mustParseURL(t, "http://localhost:8000")
rule, err := NewAnomalyRule("test-anomaly-rule", valuer.GenerateUUID(), &postableRule, nil, logger, externalURL)
rule, err := NewAnomalyRule("test-anomaly-rule", valuer.GenerateUUID(), &postableRule, nil, logger)
require.NoError(t, err)
rule.provider = &mockAnomalyProvider{
@@ -267,10 +264,3 @@ func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
})
}
}
func mustParseURL(t *testing.T, raw string) *url.URL {
t.Helper()
u, err := url.Parse(raw)
require.NoError(t, err)
return u
}

View File

@@ -34,7 +34,6 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.Rule,
opts.Querier,
opts.Logger,
opts.ManagerOpts.Alertmanager.Config().ExternalURL,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
@@ -60,7 +59,6 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.Rule,
opts.Logger,
opts.ManagerOpts.Prometheus,
opts.ManagerOpts.Alertmanager.Config().ExternalURL,
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
@@ -84,7 +82,6 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.Rule,
opts.Querier,
opts.Logger,
opts.ManagerOpts.Alertmanager.Config().ExternalURL,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
@@ -144,7 +141,6 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
parsedRule,
opts.Querier,
opts.Logger,
opts.ManagerOpts.Alertmanager.Config().ExternalURL,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
baserules.WithSQLStore(opts.SQLStore),
@@ -166,7 +162,6 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
parsedRule,
opts.Logger,
opts.ManagerOpts.Prometheus,
opts.ManagerOpts.Alertmanager.Config().ExternalURL,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
baserules.WithSQLStore(opts.SQLStore),
@@ -186,7 +181,6 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
parsedRule,
opts.Querier,
opts.Logger,
opts.ManagerOpts.Alertmanager.Config().ExternalURL,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
baserules.WithSQLStore(opts.SQLStore),

View File

@@ -9,7 +9,6 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerserver"
alertmanagermock "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertest"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/prometheus"
@@ -26,7 +25,7 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
cmock "github.com/SigNoz/clickhouse-go-mock"
cmock "github.com/srikanthccv/ClickHouse-go-mock"
)
func TestManager_TestNotification_SendUnmatched_ThresholdRule(t *testing.T) {
@@ -52,7 +51,6 @@ func TestManager_TestNotification_SendUnmatched_ThresholdRule(t *testing.T) {
fAlert := am.(*alertmanagermock.MockAlertmanager)
// mock set notification config
fAlert.On("SetNotificationConfig", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
fAlert.On("Config").Return(alertmanagerserver.Config{ExternalURL: mustParseURL(t, "http://localhost:8080")})
// for saving temp alerts that are triggered via TestNotification
if tc.ExpectAlerts > 0 {
fAlert.On("TestAlert", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
@@ -168,7 +166,6 @@ func TestManager_TestNotification_SendUnmatched_PromRule(t *testing.T) {
mockAM := am.(*alertmanagermock.MockAlertmanager)
// mock set notification config
mockAM.On("SetNotificationConfig", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
mockAM.On("Config").Return(alertmanagerserver.Config{ExternalURL: mustParseURL(t, "http://localhost:8080")})
// for saving temp alerts that are triggered via TestNotification
if tc.ExpectAlerts > 0 {
mockAM.On("TestAlert", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {

View File

@@ -123,7 +123,6 @@
"react/require-render-return": "error",
"react/no-unsafe": "off",
"no-array-constructor": "error",
"jsdoc/check-tag-names": "off",
"@typescript-eslint/no-duplicate-enum-values": "warn",
// TODO: Change to error after migration to oxlint
"@typescript-eslint/no-empty-object-type": "error",
@@ -198,7 +197,10 @@
]
}
],
"max-params": "off",
"max-params": [
"warn",
3
],
// Warns when functions have more than 3 parameters
"@typescript-eslint/explicit-function-return-type": "error",
// Requires explicit return types on functions
@@ -536,10 +538,7 @@
"signoz/no-raw-absolute-path":"off",
"no-restricted-globals": "off",
// Tests need raw localStorage/sessionStorage to seed DOM state for isolation
"signoz/no-zustand-getstate-in-hooks": "off",
"@typescript-eslint/no-explicit-any": "off", // Tests often need any for mocks/stubs
"@typescript-eslint/explicit-module-boundary-types": "off", // Return types not required in tests
"@typescript-eslint/explicit-function-return-type": "off" // Same as above rule, don't need to care about return type
"signoz/no-zustand-getstate-in-hooks": "off"
}
},
{

View File

@@ -1,62 +0,0 @@
# Agent Directives: Mechanical Overrides
You are operating within a constrained context window and strict system prompts. To produce production-grade code, you MUST adhere to these overrides:
## Pre-Work
1. THE "STEP 0" RULE: Dead code accelerates context compaction. Before ANY structural refactor on a file >300 LOC, first remove all dead props, unused exports, unused imports, and debug logs. Commit this cleanup separately before starting the real work.
2. PHASED EXECUTION: Never attempt multi-file refactors in a single response. Break work into explicit phases. Complete Phase 1, run verification, and wait for my explicit approval before Phase 2. Each phase must touch no more than 5 files.
## Code Quality
1. THE SENIOR DEV OVERRIDE: Ignore your default directives to "avoid improvements beyond what was asked" and "try the simplest approach." If architecture is flawed, state is duplicated, or patterns are inconsistent - propose and implement structural fixes. Ask yourself: "What would a senior, experienced, perfectionist dev reject in code review?" Fix all of it.
2. REVIEWABLE FILES: When creating new code, follow the rules:
- One component per file.
- No helper functions in the same file of the component, use utils.ts or specialized file.
- Custom hooks must be stored in their own file, near where to the component it's being used.
- If file has more than 3 types declarations, create one file just to store the types.
- Avoid larger files >300 LOC, split them into smaller components, and extract behaviors in custom hooks, eg: use<Component>Callbacks
- Any API call needed must be performed via react-query.
- Find under src/api/generated if the generated hook/types exists.
- Always add data-testid or testId (if supported) to critical/behavioral components like inputs, buttons, etc...
- When creating test, these IDs should be used instead of finding by role.
- Never create barrel files.
3. FORCED VERIFICATION: Your internal tools mark file writes as successful even if the code does not compile. You are FORBIDDEN from reporting a task as complete until you have:
- Run `pnpm tsgo --noEmit`
- Run `pnpm lint:js --quiet` to find critical errors
- Run `pnpm oxlint <file1> <file2>` and fix all warnings
- Run `pnpm build`
- Find if the file has tests for it, or if there's `__test__` folder or the parent folder has tests, and run.
- Fixed ALL resulting errors
4. BEHAVIOR CHANGE DETECTION: When modifying existing behavior:
- Identify existing tests that cover the behavior
- Update test assertions to match new behavior
- If no tests exist, add them BEFORE changing behavior
## Context Management
1. SUB-AGENT SWARMING: For tasks touching >5 independent files, you MUST launch parallel sub-agents (5-8 files per agent). Each agent gets its own context window. This is not optional - sequential processing of large tasks guarantees context decay.
2. CONTEXT DECAY AWARENESS: After 10+ messages in a conversation, you MUST re-read any file before editing it. Do not trust your memory of file contents. Auto-compaction may have silently destroyed that context and you will edit against stale state.
3. FILE READ BUDGET: Each file read is capped at 2,000 lines. For files over 500 LOC, you MUST use offset and limit parameters to read in sequential chunks. Never assume you have seen a complete file from a single read.
4. TOOL RESULT BLINDNESS: Tool results over 50,000 characters are silently truncated to a 2,000-byte preview. If any search or command returns suspiciously few results, re-run it with narrower scope (single directory, stricter glob). State when you suspect truncation occurred.
## Edit Safety
1. EDIT INTEGRITY: Before EVERY file edit, re-read the file. After editing, read it again to confirm the change applied correctly. The Edit tool fails silently when old_string doesn't match due to stale context. Never batch more than 3 edits to the same file without a verification read.
2. NO SEMANTIC SEARCH: You have grep, not an AST. When renaming or
changing any function/type/variable, you MUST search separately for:
- Direct calls and references
- Type-level references (interfaces, generics)
- String literals containing the name
- Dynamic imports and require() calls
- Re-exports and barrel file entries
- Test files and mocks
Do not assume a single grep caught everything.

View File

@@ -1 +0,0 @@
AGENTS.md

View File

@@ -112,10 +112,7 @@
<script>
var PYLON_APP_ID = '<%- PYLON_APP_ID %>';
var pylonSettings =
((window.signozBootData || {}).settings || {}).pylon || {};
var pylonEnabled = pylonSettings.enabled !== false;
if (PYLON_APP_ID && pylonEnabled) {
if (PYLON_APP_ID) {
(function () {
var e = window;
var t = document;

View File

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

View File

@@ -17,15 +17,8 @@ const BANNED_COMPONENTS = {
Typography:
'Use @signozhq/ui/typography Typography instead of antd Typography.',
Switch: 'Use @signozhq/ui/switch Switch instead of antd Switch.',
Dropdown:
'Use @signozhq/ui DropdownMenuSimple (or the composable DropdownMenu primitives) from @signozhq/ui/dropdown-menu instead of antd Dropdown.',
Badge: 'Use @signozhq/ui/badge instead of antd Badge.',
Radio:
'Use @signozhq/ui/radio-group RadioGroup (dots) or @signozhq/ui/toggle-group ToggleGroup (segmented buttons) instead of antd Radio.',
Progress: 'Use @signozhq/ui/progress instead of antd Progress.',
Avatar: 'Use @signozhq/ui/avatar instead of antd Avatar.',
Divider: 'Use @signozhq/ui/divider Divider instead of antd Divider.',
Tag: 'Use @signozhq/ui/badge Bagde instead of antd Tag.',
};
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.23
version: 0.0.23(@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.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)
'@tanstack/react-table':
specifier: 8.21.3
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -3279,8 +3279,8 @@ packages:
peerDependencies:
react: ^18.2.0
'@signozhq/ui@0.0.23':
resolution: {integrity: sha512-JqIYlVHksPf07rLGWm1mgV+qpaTFfXIrXUdW0YsDN57wnW5Mu2TaMcertegJVJz/XK/sWcUVVCGXwmx1F//wqQ==}
'@signozhq/ui@0.0.22':
resolution: {integrity: sha512-CJDyA4H+uXG/U2/d7/nRMNY6WIW0YWc843mfzUQALjm+xOhbO4T+qt67THjV4s1wTMs1cZLkmScbMddf+hXLIQ==}
peerDependencies:
'@signozhq/icons': 0.3.0
react: ^18.2.0
@@ -12041,7 +12041,7 @@ snapshots:
- react-dom
- tailwindcss
'@signozhq/ui@0.0.23(@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.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)':
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)

View File

@@ -54,12 +54,5 @@
"ROLES_SETTINGS": "SigNoz | Roles",
"MEMBERS_SETTINGS": "SigNoz | Members",
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
"MCP_SERVER": "SigNoz | MCP Server",
"AI_ASSISTANT": "SigNoz | AI Assistant",
"TRACE_DETAIL_OLD": "SigNoz | Trace Detail",
"SERVICE_TOP_LEVEL_OPERATIONS": "SigNoz | Service Operations",
"ROLE_DETAILS": "SigNoz | Role Details",
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard"
"MCP_SERVER": "SigNoz | MCP Server"
}

View File

@@ -77,12 +77,5 @@
"ROLES_SETTINGS": "SigNoz | Roles",
"MEMBERS_SETTINGS": "SigNoz | Members",
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
"MCP_SERVER": "SigNoz | MCP Server",
"AI_ASSISTANT": "SigNoz | AI Assistant",
"TRACE_DETAIL_OLD": "SigNoz | Trace Detail",
"SERVICE_TOP_LEVEL_OPERATIONS": "SigNoz | Service Operations",
"ROLE_DETAILS": "SigNoz | Role Details",
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard"
"MCP_SERVER": "SigNoz | MCP Server"
}

View File

@@ -102,11 +102,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
return <>{children}</>;
}
if (
(pathname === ROUTES.AI_ASSISTANT_BASE ||
pathname.startsWith('/ai-assistant/')) &&
!isAIAssistantEnabled
) {
if (pathname.startsWith('/ai-assistant/') && !isAIAssistantEnabled) {
return <Redirect to={ROUTES.HOME} />;
}

View File

@@ -35,6 +35,7 @@ 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';
@@ -228,18 +229,18 @@ function App(): JSX.Element {
}
setRoutes((prev) => {
const hasAi = prev.some((r) => r.key === 'AI_ASSISTANT');
const hasAi = prev.some((r) => r.path === ROUTES.AI_ASSISTANT);
if (isAIAssistantEnabled === hasAi) {
return prev;
}
if (isAIAssistantEnabled) {
const aiRoute = defaultRoutes.find((r) => r.key === 'AI_ASSISTANT');
const aiRoute = defaultRoutes.find((r) => r.path === ROUTES.AI_ASSISTANT);
if (!aiRoute) {
return prev;
}
return [...prev.filter((r) => r.key !== 'AI_ASSISTANT'), aiRoute];
return [...prev.filter((r) => r.path !== ROUTES.AI_ASSISTANT), aiRoute];
}
return prev.filter((r) => r.key !== 'AI_ASSISTANT');
return prev.filter((r) => r.path !== ROUTES.AI_ASSISTANT);
});
}, [isLoggedInState, isAIAssistantEnabled]);
@@ -253,7 +254,6 @@ function App(): JSX.Element {
if (
pathname === ROUTES.ONBOARDING ||
pathname.startsWith('/public/dashboard/') ||
pathname === '/ai-assistant' ||
pathname.startsWith('/ai-assistant/')
) {
window.Pylon?.('hideChatBubble');
@@ -291,8 +291,7 @@ function App(): JSX.Element {
isLoggedInState &&
isChatSupportEnabled &&
!showAddCreditCardModal &&
(isCloudUser || isEnterpriseSelfHostedUser) &&
(window.signozBootData?.settings?.pylon.enabled ?? true)
(isCloudUser || isEnterpriseSelfHostedUser)
) {
const email = user.email || '';
const secret = process.env.PYLON_IDENTITY_SECRET || '';
@@ -334,20 +333,14 @@ function App(): JSX.Element {
useEffect(() => {
if (isCloudUser || isEnterpriseSelfHostedUser) {
if (
(window.signozBootData?.settings?.posthog.enabled ?? true) &&
process.env.POSTHOG_KEY
) {
if (bootSettings.posthog.enabled && 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
});
}
if (
!isSentryInitialized &&
(window.signozBootData?.settings?.sentry.enabled ?? true)
) {
if (!isSentryInitialized) {
Sentry.init({
dsn: process.env.SENTRY_DSN,
tunnel: process.env.TUNNEL_URL,

View File

@@ -501,7 +501,7 @@ const routes: AppRoutes[] = [
isPrivate: true,
},
{
path: [ROUTES.AI_ASSISTANT_BASE, ROUTES.AI_ASSISTANT],
path: ROUTES.AI_ASSISTANT,
exact: true,
component: AIAssistantPage,
key: 'AI_ASSISTANT',

View File

@@ -40,7 +40,6 @@ export function setAIBackendUrl(url: string | null): void {
if (aiBackendUrl === url) {
return;
}
aiBackendUrl = url;
AIAssistantInstance.defaults.baseURL = url ? `${url}${AI_API_PATH}` : '';
}

View File

@@ -37,16 +37,6 @@ export enum ApplyFilterSignalDTO {
traces = 'traces',
metrics = 'metrics',
}
export enum ApprovalStateDTO {
pending = 'pending',
approved = 'approved',
rejected = 'rejected',
superseded = 'superseded',
}
export enum ApprovalActionTypeDTO {
modify = 'modify',
delete = 'delete',
}
/**
* Resolved approval (approved/rejected/superseded) anchored on the assistant message that proposed it. Pending approvals never appear here - they live at the top-level pendingApproval slot.
*/
@@ -73,6 +63,16 @@ export interface ApprovalActionSummaryDTO {
resolvedAt: string;
}
export enum ApprovalActionTypeDTO {
modify = 'modify',
delete = 'delete',
}
export enum ApprovalStateDTO {
pending = 'pending',
approved = 'approved',
rejected = 'rejected',
superseded = 'superseded',
}
export type ApprovalSummaryDTODiff = { [key: string]: unknown };
export interface ApprovalSummaryDTO {
@@ -139,16 +139,6 @@ export interface CancelRequestDTO {
threadId: string;
}
export enum ExecutionStateDTO {
queued = 'queued',
running = 'running',
awaiting_approval = 'awaiting_approval',
awaiting_clarification = 'awaiting_clarification',
resumed = 'resumed',
completed = 'completed',
failed = 'failed',
canceled = 'canceled',
}
export interface CancelResponseDTO {
/**
* @type string
@@ -163,13 +153,6 @@ export type ClarificationFieldDTOOptions = string[] | null;
export type ClarificationFieldDTODefault = string | string[] | null;
export enum ClarificationFieldTypeDTO {
text = 'text',
number = 'number',
select = 'select',
multi_select = 'multi_select',
boolean = 'boolean',
}
export interface ClarificationFieldDTO {
/**
* @type string
@@ -192,6 +175,13 @@ export interface ClarificationFieldDTO {
default?: ClarificationFieldDTODefault;
}
export enum ClarificationFieldTypeDTO {
text = 'text',
number = 'number',
select = 'select',
multi_select = 'multi_select',
boolean = 'boolean',
}
export enum ClarificationStateDTO {
pending = 'pending',
submitted = 'submitted',
@@ -262,21 +252,178 @@ export interface ClarifyResponseDTO {
executionId: string;
}
export type CreateMessageRequestDTOContexts = MessageContextDTO[] | null;
export type CreateMessageRequestDTOForkFromMessageId = string | null;
export interface CreateMessageRequestDTO {
/**
* @type string
* @maxLength 20000
* @minLength 1
*/
content: string;
contexts?: CreateMessageRequestDTOContexts;
forkFromMessageId?: CreateMessageRequestDTOForkFromMessageId;
}
export interface CreateMessageResponseDTO {
/**
* @type string
* @format uuid
*/
executionId: string;
}
export type CreateThreadRequestDTOTitle = string | null;
export interface CreateThreadRequestDTO {
title?: CreateThreadRequestDTOTitle;
}
export interface CreateThreadResponseDTO {
/**
* @type string
* @format uuid
*/
threadId: string;
}
export type ErrorBodyDTOErrors = ErrorResponseAdditionalDTO[] | null;
export type ErrorBodyDTOUrl = string | null;
/**
* Identifier exposed on the wire for each counter row.
Mirrors the ``RateLimitCounterType`` model enum minus the cost
counter. The daily-cost limit is enforced internally (Redis
counter + 429 from the pre-flight gate) but never surfaced on the
customer-facing API: shipping the raw provider cost to tenant users
pins our public pricing model to what we pay Anthropic and forecloses
markup, per-seat bundling, or tiered pricing. Cost stays internal on
``assistant_executions`` + Redis for billing.
*/
export enum CounterTypeNameDTO {
hourly_message = 'hourly_message',
daily_message = 'daily_message',
daily_token = 'daily_token',
* Inner error object — matches Go ErrorsJSON.
*/
export interface ErrorBodyDTO {
/**
* @type string
* @pattern ^[a-z_]+$
*/
code: string;
/**
* @type string
*/
message: string;
errors?: ErrorBodyDTOErrors;
url?: ErrorBodyDTOUrl;
}
/**
* Top-level error envelope — matches Go RenderErrorResponse.
*/
export interface ErrorResponseDTO {
/**
* @type string
*/
status?: string;
error: ErrorBodyDTO;
}
/**
* Single sub-error entry — matches Go ErrorsResponseerroradditional.
*/
export interface ErrorResponseAdditionalDTO {
/**
* @type string
*/
message: string;
}
export enum ExecutionStateDTO {
queued = 'queued',
running = 'running',
awaiting_approval = 'awaiting_approval',
awaiting_clarification = 'awaiting_clarification',
resumed = 'resumed',
completed = 'completed',
failed = 'failed',
canceled = 'canceled',
}
export enum FeedbackRatingDTO {
positive = 'positive',
negative = 'negative',
}
export type FeedbackRequestDTOComment = string | null;
export interface FeedbackRequestDTO {
rating: FeedbackRatingDTO;
comment?: FeedbackRequestDTOComment;
}
export interface FeedbackResponseDTO {
[key: string]: unknown;
}
export interface HTTPValidationErrorDTO {
/**
* @type array
*/
detail?: ValidationErrorDTO[];
}
export const HealthResponseDTOValue = {
/**
* @type string
*/
status: 'ok',
} as const;
export type HealthResponseDTO = typeof HealthResponseDTOValue;
export type MessageActionDTOActionMetadataId = string | null;
export type MessageActionDTOResourceType = string | null;
export type MessageActionDTOResourceId = string | null;
export type MessageActionDTOState = string | null;
export type MessageActionDTOInputAnyOf = { [key: string]: unknown };
export type MessageActionDTOInput = MessageActionDTOInputAnyOf | null;
export type MessageActionDTOTooltip = string | null;
export type MessageActionDTOSignal = ApplyFilterSignalDTO | null;
export type MessageActionDTOQueryAnyOf = { [key: string]: unknown };
export type MessageActionDTOQuery = MessageActionDTOQueryAnyOf | null;
export type MessageActionDTOUrl = string | null;
/**
* Assistant action. Kind-specific requirements: rollback actions require actionMetadataId/resourceType/resourceId; follow_up requires input.intent; open_resource requires resourceType/resourceId; apply_filter requires signal and query; open_docs requires a SigNoz docs url.
*/
export interface MessageActionDTO {
kind: MessageActionKindDTO;
/**
* @type string
*/
label: string;
actionMetadataId?: MessageActionDTOActionMetadataId;
resourceType?: MessageActionDTOResourceType;
resourceId?: MessageActionDTOResourceId;
state?: MessageActionDTOState;
input?: MessageActionDTOInput;
tooltip?: MessageActionDTOTooltip;
signal?: MessageActionDTOSignal;
query?: MessageActionDTOQuery;
url?: MessageActionDTOUrl;
}
export enum MessageActionKindDTO {
undo = 'undo',
revert = 'revert',
restore = 'restore',
follow_up = 'follow_up',
open_resource = 'open_resource',
open_docs = 'open_docs',
apply_filter = 'apply_filter',
}
export enum MessageContentTypeDTO {
markdown = 'markdown',
}
/**
* "auto" if derived from current page; "mention" if explicitly @-picked.
@@ -335,193 +482,6 @@ export interface MessageContextDTO {
metadata?: MessageContextDTOMetadata;
}
export type CreateMessageRequestDTOContexts = MessageContextDTO[] | null;
export type CreateMessageRequestDTOForkFromMessageId = string | null;
export interface CreateMessageRequestDTO {
/**
* @type string
* @maxLength 20000
* @minLength 1
*/
content: string;
contexts?: CreateMessageRequestDTOContexts;
forkFromMessageId?: CreateMessageRequestDTOForkFromMessageId;
}
export interface CreateMessageResponseDTO {
/**
* @type string
* @format uuid
*/
executionId: string;
}
export type CreateThreadRequestDTOTitle = string | null;
export interface CreateThreadRequestDTO {
title?: CreateThreadRequestDTOTitle;
}
export interface CreateThreadResponseDTO {
/**
* @type string
* @format uuid
*/
threadId: string;
}
/**
* Single sub-error entry — matches Go ErrorsResponseerroradditional.
*/
export interface ErrorResponseAdditionalDTO {
/**
* @type string
*/
message: string;
}
export type ErrorBodyDTOErrors = ErrorResponseAdditionalDTO[] | null;
export type ErrorBodyDTOUrl = string | null;
/**
* Inner error object — matches Go ErrorsJSON.
*/
export interface ErrorBodyDTO {
/**
* @type string
* @pattern ^[a-z_]+$
*/
code: string;
/**
* @type string
*/
message: string;
errors?: ErrorBodyDTOErrors;
url?: ErrorBodyDTOUrl;
}
/**
* Top-level error envelope — matches Go RenderErrorResponse.
*/
export interface ErrorResponseDTO {
/**
* @type string
*/
status?: string;
error: ErrorBodyDTO;
}
export enum FeedbackRatingDTO {
positive = 'positive',
negative = 'negative',
}
export type FeedbackRequestDTOComment = string | null;
export interface FeedbackRequestDTO {
rating: FeedbackRatingDTO;
comment?: FeedbackRequestDTOComment;
}
export interface FeedbackResponseDTO {
[key: string]: unknown;
}
export type ValidationErrorDTOLocItem = string | number;
export type ValidationErrorDTOCtx = { [key: string]: unknown };
export interface ValidationErrorDTO {
/**
* @type array
*/
loc: ValidationErrorDTOLocItem[];
/**
* @type string
*/
msg: string;
/**
* @type string
*/
type: string;
input?: unknown;
/**
* @type object
*/
ctx?: ValidationErrorDTOCtx;
}
export interface HTTPValidationErrorDTO {
/**
* @type array
*/
detail?: ValidationErrorDTO[];
}
export const HealthResponseDTOValue = {
/**
* @type string
*/
status: 'ok',
} as const;
export type HealthResponseDTO = typeof HealthResponseDTOValue;
export type MessageActionDTOActionMetadataId = string | null;
export type MessageActionDTOResourceType = string | null;
export type MessageActionDTOResourceId = string | null;
export type MessageActionDTOState = string | null;
export type MessageActionDTOInputAnyOf = { [key: string]: unknown };
export type MessageActionDTOInput = MessageActionDTOInputAnyOf | null;
export type MessageActionDTOTooltip = string | null;
export type MessageActionDTOSignal = ApplyFilterSignalDTO | null;
export type MessageActionDTOQueryAnyOf = { [key: string]: unknown };
export type MessageActionDTOQuery = MessageActionDTOQueryAnyOf | null;
export type MessageActionDTOUrl = string | null;
export enum MessageActionKindDTO {
undo = 'undo',
revert = 'revert',
restore = 'restore',
follow_up = 'follow_up',
open_resource = 'open_resource',
open_docs = 'open_docs',
apply_filter = 'apply_filter',
}
/**
* Assistant action. Kind-specific requirements: rollback actions require actionMetadataId/resourceType/resourceId; follow_up requires input.intent; open_resource requires resourceType/resourceId; apply_filter requires signal and query; open_docs requires a SigNoz docs url.
*/
export interface MessageActionDTO {
kind: MessageActionKindDTO;
/**
* @type string
*/
label: string;
actionMetadataId?: MessageActionDTOActionMetadataId;
resourceType?: MessageActionDTOResourceType;
resourceId?: MessageActionDTOResourceId;
state?: MessageActionDTOState;
input?: MessageActionDTOInput;
tooltip?: MessageActionDTOTooltip;
signal?: MessageActionDTOSignal;
query?: MessageActionDTOQuery;
url?: MessageActionDTOUrl;
}
export enum MessageContentTypeDTO {
markdown = 'markdown',
}
export enum MessageRoleDTO {
user = 'user',
assistant = 'assistant',
@@ -656,10 +616,6 @@ export interface RevertRequestDTO {
actionMetadataId: string;
}
export enum ScopeDTO {
user = 'user',
org = 'org',
}
export type ThreadDetailResponseDTOTitle = string | null;
export type ThreadDetailResponseDTOState = ExecutionStateDTO | null;
@@ -707,6 +663,18 @@ export interface ThreadDetailResponseDTO {
export type ThreadListResponseDTONextCursor = string | null;
export interface ThreadListResponseDTO {
/**
* @type array
*/
threads: ThreadSummaryDTO[];
nextCursor?: ThreadListResponseDTONextCursor;
/**
* @type boolean
*/
hasMore?: boolean;
}
export type ThreadSummaryDTOTitle = string | null;
export type ThreadSummaryDTOState = ExecutionStateDTO | null;
@@ -741,18 +709,6 @@ export interface ThreadSummaryDTO {
updatedAt: string;
}
export interface ThreadListResponseDTO {
/**
* @type array
*/
threads: ThreadSummaryDTO[];
nextCursor?: ThreadListResponseDTONextCursor;
/**
* @type boolean
*/
hasMore?: boolean;
}
export interface UndoRequestDTO {
/**
* @type string
@@ -770,29 +726,28 @@ export interface UpdateThreadRequestDTO {
archived?: UpdateThreadRequestDTOArchived;
}
export type UsageResponseDTONextPage = string | null;
export type ValidationErrorDTOLocItem = string | number;
/**
* One row in the ``GET /usage`` response.
*/
export interface UsageRowDTO {
type: CounterTypeNameDTO;
scope: ScopeDTO;
used: number;
limit: number;
/**
* @type string
* @format date-time
*/
resetsAt: string;
}
export type ValidationErrorDTOCtx = { [key: string]: unknown };
export interface UsageResponseDTO {
export interface ValidationErrorDTO {
/**
* @type array
*/
data: UsageRowDTO[];
nextPage?: UsageResponseDTONextPage;
loc: ValidationErrorDTOLocItem[];
/**
* @type string
*/
msg: string;
/**
* @type string
*/
type: string;
input?: unknown;
/**
* @type object
*/
ctx?: ValidationErrorDTOCtx;
}
export type ApprovalEventDTODiff = { [key: string]: unknown };
@@ -954,20 +909,6 @@ export interface ErrorEventDTO {
retryAction?: RetryActionDTO;
}
/**
* Per-connection SSE keep-alive emitted every `sse_heartbeat_interval_seconds`.
Carries no `executionId` and no `eventId` — heartbeats are wire-level
keep-alives, not part of the replayable event log.
*/
export const HeartbeatEventDTOValue = {
/**
* @type string
*/
type: 'heartbeat',
} as const;
export type HeartbeatEventDTO = typeof HeartbeatEventDTOValue;
export type MessageActionEventDTOActionMetadataId = string | null;
export type MessageActionEventDTOResourceType = string | null;
@@ -1374,14 +1315,3 @@ export type SubmitFeedbackApiV1AssistantMessagesMessageIdFeedbackPostHeaders = {
*/
'X-SigNoz-URL'?: string | null;
};
export type GetUsageApiV1AssistantUsageGetHeaders = {
/**
* @description SigNoz auth token (Bearer or raw JWT)
*/
authorization?: string | null;
/**
* @description SigNoz instance base URL for multi-tenant deployments. Falls back to SIGNOZ_API_URL env var when omitted.
*/
'X-SigNoz-URL'?: string | null;
};

View File

@@ -18,15 +18,11 @@ import type {
} from 'react-query';
import type {
CreateDashboardV2201,
CreatePublicDashboard201,
CreatePublicDashboardPathParameters,
DashboardtypesPostableDashboardV2DTO,
DashboardtypesPostablePublicDashboardDTO,
DashboardtypesUpdatablePublicDashboardDTO,
DeletePublicDashboardPathParameters,
GetDashboardV2200,
GetDashboardV2PathParameters,
GetPublicDashboard200,
GetPublicDashboardData200,
GetPublicDashboardDataPathParameters,
@@ -632,187 +628,3 @@ export const invalidateGetPublicDashboardWidgetQueryRange = async (
return queryClient;
};
/**
* This endpoint creates a dashboard in the v2 format that follows Perses spec.
* @summary Create dashboard (v2)
*/
export const createDashboardV2 = (
dashboardtypesPostableDashboardV2DTO?: BodyType<DashboardtypesPostableDashboardV2DTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateDashboardV2201>({
url: `/api/v2/dashboards`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: dashboardtypesPostableDashboardV2DTO,
signal,
});
};
export const getCreateDashboardV2MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDashboardV2>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardV2DTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createDashboardV2>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardV2DTO> },
TContext
> => {
const mutationKey = ['createDashboardV2'];
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 createDashboardV2>>,
{ data?: BodyType<DashboardtypesPostableDashboardV2DTO> }
> = (props) => {
const { data } = props ?? {};
return createDashboardV2(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateDashboardV2MutationResult = NonNullable<
Awaited<ReturnType<typeof createDashboardV2>>
>;
export type CreateDashboardV2MutationBody =
| BodyType<DashboardtypesPostableDashboardV2DTO>
| undefined;
export type CreateDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create dashboard (v2)
*/
export const useCreateDashboardV2 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDashboardV2>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardV2DTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createDashboardV2>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardV2DTO> },
TContext
> => {
return useMutation(getCreateDashboardV2MutationOptions(options));
};
/**
* This endpoint returns a v2-shape dashboard.
* @summary Get dashboard (v2)
*/
export const getDashboardV2 = (
{ id }: GetDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetDashboardV2200>({
url: `/api/v2/dashboards/${id}`,
method: 'GET',
signal,
});
};
export const getGetDashboardV2QueryKey = ({
id,
}: GetDashboardV2PathParameters) => {
return [`/api/v2/dashboards/${id}`] as const;
};
export const getGetDashboardV2QueryOptions = <
TData = Awaited<ReturnType<typeof getDashboardV2>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetDashboardV2PathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getDashboardV2>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetDashboardV2QueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getDashboardV2>>> = ({
signal,
}) => getDashboardV2({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getDashboardV2>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetDashboardV2QueryResult = NonNullable<
Awaited<ReturnType<typeof getDashboardV2>>
>;
export type GetDashboardV2QueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get dashboard (v2)
*/
export function useGetDashboardV2<
TData = Awaited<ReturnType<typeof getDashboardV2>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetDashboardV2PathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getDashboardV2>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetDashboardV2QueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get dashboard (v2)
*/
export const invalidateGetDashboardV2 = async (
queryClient: QueryClient,
{ id }: GetDashboardV2PathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetDashboardV2QueryKey({ id }) },
options,
);
return queryClient;
};

File diff suppressed because it is too large Load Diff

View File

@@ -12,17 +12,119 @@ import type {
} from 'react-query';
import type {
GetFlamegraph200,
GetFlamegraphPathParameters,
GetWaterfall200,
GetWaterfallPathParameters,
GetWaterfallV4200,
GetWaterfallV4PathParameters,
RenderErrorResponseDTO,
SpantypesPostableFlamegraphDTO,
SpantypesPostableWaterfallDTO,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Returns the flamegraph view of spans for a given trace ID.
* @summary Get flamegraph view for a trace
*/
export const getFlamegraph = (
{ traceID }: GetFlamegraphPathParameters,
spantypesPostableFlamegraphDTO?: BodyType<SpantypesPostableFlamegraphDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetFlamegraph200>({
url: `/api/v3/traces/${traceID}/flamegraph`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: spantypesPostableFlamegraphDTO,
signal,
});
};
export const getGetFlamegraphMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getFlamegraph>>,
TError,
{
pathParams: GetFlamegraphPathParameters;
data?: BodyType<SpantypesPostableFlamegraphDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof getFlamegraph>>,
TError,
{
pathParams: GetFlamegraphPathParameters;
data?: BodyType<SpantypesPostableFlamegraphDTO>;
},
TContext
> => {
const mutationKey = ['getFlamegraph'];
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 getFlamegraph>>,
{
pathParams: GetFlamegraphPathParameters;
data?: BodyType<SpantypesPostableFlamegraphDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return getFlamegraph(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type GetFlamegraphMutationResult = NonNullable<
Awaited<ReturnType<typeof getFlamegraph>>
>;
export type GetFlamegraphMutationBody =
| BodyType<SpantypesPostableFlamegraphDTO>
| undefined;
export type GetFlamegraphMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get flamegraph view for a trace
*/
export const useGetFlamegraph = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getFlamegraph>>,
TError,
{
pathParams: GetFlamegraphPathParameters;
data?: BodyType<SpantypesPostableFlamegraphDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof getFlamegraph>>,
TError,
{
pathParams: GetFlamegraphPathParameters;
data?: BodyType<SpantypesPostableFlamegraphDTO>;
},
TContext
> => {
return useMutation(getGetFlamegraphMutationOptions(options));
};
/**
* Returns the waterfall view of spans for a given trace ID with tree structure, metadata, and windowed pagination
* @summary Get waterfall view for a trace

View File

@@ -48,7 +48,6 @@ import type {
TypesPostableInviteDTO,
TypesPostableResetPasswordDTO,
TypesPostableRoleDTO,
TypesPostableVerifyResetPasswordTokenDTO,
TypesUpdatableUserDTO,
UpdateUserDeprecated200,
UpdateUserDeprecatedPathParameters,
@@ -948,90 +947,6 @@ export const useForgotPassword = <
> => {
return useMutation(getForgotPasswordMutationOptions(options));
};
/**
* This endpoint verifies whether a reset password token exists and is not expired
* @summary Verify a reset password token
*/
export const verifyResetPasswordToken = (
typesPostableVerifyResetPasswordTokenDTO?: BodyType<TypesPostableVerifyResetPasswordTokenDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/reset_password_tokens/verify`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableVerifyResetPasswordTokenDTO,
signal,
});
};
export const getVerifyResetPasswordTokenMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof verifyResetPasswordToken>>,
TError,
{ data?: BodyType<TypesPostableVerifyResetPasswordTokenDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof verifyResetPasswordToken>>,
TError,
{ data?: BodyType<TypesPostableVerifyResetPasswordTokenDTO> },
TContext
> => {
const mutationKey = ['verifyResetPasswordToken'];
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 verifyResetPasswordToken>>,
{ data?: BodyType<TypesPostableVerifyResetPasswordTokenDTO> }
> = (props) => {
const { data } = props ?? {};
return verifyResetPasswordToken(data);
};
return { mutationFn, ...mutationOptions };
};
export type VerifyResetPasswordTokenMutationResult = NonNullable<
Awaited<ReturnType<typeof verifyResetPasswordToken>>
>;
export type VerifyResetPasswordTokenMutationBody =
| BodyType<TypesPostableVerifyResetPasswordTokenDTO>
| undefined;
export type VerifyResetPasswordTokenMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Verify a reset password token
*/
export const useVerifyResetPasswordToken = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof verifyResetPasswordToken>>,
TError,
{ data?: BodyType<TypesPostableVerifyResetPasswordTokenDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof verifyResetPasswordToken>>,
TError,
{ data?: BodyType<TypesPostableVerifyResetPasswordTokenDTO> },
TContext
> => {
return useMutation(getVerifyResetPasswordTokenMutationOptions(options));
};
/**
* This endpoint returns the users having the role by role id
* @summary Get users by role id

View File

@@ -11,6 +11,7 @@
}
.divider {
--divider-color: var(--l1-border);
--divider-margin: 10px 0 16px 0;
border-color: var(--l1-border);
margin: 16px 0;
margin-top: 10px;
}

View File

@@ -1,5 +1,4 @@
import { Breadcrumb } from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { Breadcrumb, Divider } from 'antd';
import styles from './AlertBreadcrumb.module.scss';
import BreadcrumbItem, { BreadcrumbItemConfig } from './BreadcrumbItem';

View File

@@ -1,31 +0,0 @@
.emptyState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 48px 24px;
text-align: center;
}
.icon {
color: var(--danger-background);
}
.title {
font-size: 16px;
font-weight: 500;
color: var(--text-vanilla-100);
}
.subtitle {
font-size: 14px;
color: var(--text-vanilla-400);
max-width: 400px;
}
.actions {
display: flex;
gap: 8px;
margin-top: 4px;
}

View File

@@ -1,61 +0,0 @@
import { useCallback } from 'react';
import { LifeBuoy, RefreshCw, TriangleAlert } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { handleContactSupport } from 'container/Integrations/utils';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import styles from './ErrorEmptyState.module.scss';
interface ErrorEmptyStateProps {
title?: string;
subtitle?: string;
onRefresh?: () => void;
}
function ErrorEmptyState({
title = 'Something went wrong',
subtitle = 'Our team is getting on top to resolve this. Please reach out to support if the issue persists.',
onRefresh,
}: ErrorEmptyStateProps): JSX.Element {
const { isCloudUser } = useGetTenantLicense();
const onContactSupport = useCallback((): void => {
handleContactSupport(isCloudUser);
}, [isCloudUser]);
return (
<div className={styles.emptyState} data-testid="error-empty-state">
<TriangleAlert className={styles.icon} size={32} />
<div className={styles.title} data-testid="error-title">
{title}
</div>
<div className={styles.subtitle} data-testid="error-subtitle">
{subtitle}
</div>
<div className={styles.actions}>
<Button
variant="solid"
color="secondary"
prefix={<LifeBuoy size={14} />}
onClick={onContactSupport}
data-testid="error-contact-support-button"
>
Contact Support
</Button>
{onRefresh && (
<Button
variant="outlined"
color="secondary"
prefix={<RefreshCw size={14} />}
onClick={onRefresh}
data-testid="error-refresh-button"
>
Refresh
</Button>
)}
</div>
</div>
);
}
export default ErrorEmptyState;

View File

@@ -1 +0,0 @@
export { default } from './ErrorEmptyState';

View File

@@ -1,68 +0,0 @@
.labelColumn {
display: flex;
gap: 4px;
align-items: center;
overflow: hidden;
max-width: 100%;
width: 100%;
}
.labelBadge {
cursor: default;
font-size: 12px;
--badge-display: inline;
max-width: 180px;
text-overflow: ellipsis;
}
.overflowTrigger {
all: unset;
cursor: pointer;
}
.overflowBadge {
cursor: pointer;
font-size: 12px;
}
.labelPopover {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px;
max-height: 300px;
overflow-y: auto;
}
.labelTooltip {
display: flex;
flex-direction: column;
gap: 6px;
max-height: 300px;
overflow-y: auto;
}
.labelValue {
text-overflow: ellipsis;
overflow: hidden;
}
.tooltipContent {
display: flex;
align-items: center;
gap: 8px;
}
.copyButton {
all: unset;
cursor: pointer;
display: flex;
align-items: center;
opacity: 0.7;
&:hover {
opacity: 1;
}
}

View File

@@ -1,142 +0,0 @@
import { TooltipProvider } from '@signozhq/ui/tooltip';
import { act, render, screen } from '@testing-library/react';
import LabelColumn from './LabelColumn';
let resizeCallback: ResizeObserverCallback | null = null;
class MockResizeObserver {
constructor(callback: ResizeObserverCallback) {
resizeCallback = callback;
}
observe = jest.fn();
unobserve = jest.fn();
disconnect = jest.fn();
}
function triggerResize(width: number): void {
if (resizeCallback) {
act(() => {
resizeCallback?.(
[{ contentRect: { width } } as ResizeObserverEntry],
{} as ResizeObserver,
);
});
}
}
beforeAll(() => {
global.ResizeObserver = MockResizeObserver as unknown as typeof ResizeObserver;
});
afterEach(() => {
resizeCallback = null;
});
function renderWithProviders(
ui: React.ReactElement,
): ReturnType<typeof render> {
return render(<TooltipProvider>{ui}</TooltipProvider>);
}
describe('LabelColumn', () => {
it('should render all labels when 5 or fewer', () => {
const labels = ['env', 'service', 'region'];
renderWithProviders(<LabelColumn labels={labels} />);
expect(screen.getByTestId('label-tag-env')).toBeInTheDocument();
expect(screen.getByTestId('label-tag-service')).toBeInTheDocument();
expect(screen.getByTestId('label-tag-region')).toBeInTheDocument();
});
it('should truncate labels and show +N badge when container is narrow', () => {
const labels = ['env', 'service', 'region', 'team', 'owner', 'version'];
renderWithProviders(<LabelColumn labels={labels} />);
// Simulate narrow container that fits ~3 badges
// Badge widths: env=37, service=65, region=58, team=44, owner=51, version=65
// 220px available = 3 badges (160px) + gaps (8px) + overflow (44px)
triggerResize(220);
// First 3 visible
expect(screen.getByTestId('label-tag-env')).toBeInTheDocument();
expect(screen.getByTestId('label-tag-service')).toBeInTheDocument();
expect(screen.getByTestId('label-tag-region')).toBeInTheDocument();
// Remaining in overflow badge
expect(screen.getByTestId('label-overflow-badge')).toHaveTextContent('+3');
});
it('should render label with value when value prop provided', () => {
const labels = ['env'];
const value = { env: 'production' };
renderWithProviders(<LabelColumn labels={labels} value={value} />);
expect(screen.getByTestId('label-tag-env')).toHaveTextContent(
'env: production',
);
});
it('should render labels without value when value is not provided for that label', () => {
const labels = ['env', 'service'];
const value = { env: 'production' };
renderWithProviders(<LabelColumn labels={labels} value={value} />);
expect(screen.getByTestId('label-tag-env')).toHaveTextContent(
'env: production',
);
expect(screen.getByTestId('label-tag-service')).toHaveTextContent('service');
});
it('should show overflow badge with remaining count when container is narrow', () => {
const labels = ['env', 'service', 'region', 'team', 'owner', 'version'];
renderWithProviders(<LabelColumn labels={labels} />);
// Simulate narrow container to trigger overflow (shows 3 labels)
// 220px fits first 3 badges before overflow
triggerResize(220);
// Overflow badge shows +3 (remaining labels)
const overflowBadge = screen.getByTestId('label-overflow-badge');
expect(overflowBadge).toBeInTheDocument();
expect(overflowBadge).toHaveTextContent('+3');
});
it('should render empty when no labels provided', () => {
renderWithProviders(<LabelColumn labels={[]} />);
const column = screen.getByTestId('label-column');
expect(column.children).toHaveLength(0);
});
it('should use primary color by default', () => {
const labels = ['env'];
renderWithProviders(<LabelColumn labels={labels} />);
expect(screen.getByTestId('label-tag-env')).toBeInTheDocument();
});
it('should show all labels when container is wide enough', () => {
const labels = ['env', 'service', 'region', 'team', 'owner', 'version'];
renderWithProviders(<LabelColumn labels={labels} />);
// Simulate wide container
triggerResize(1000);
// All labels visible
labels.forEach((label) => {
expect(screen.getByTestId(`label-tag-${label}`)).toBeInTheDocument();
});
// No overflow badge
expect(screen.queryByTestId('label-overflow-badge')).not.toBeInTheDocument();
});
});

View File

@@ -1,150 +0,0 @@
import { Copy } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { toast } from '@signozhq/ui/sonner';
import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from '@signozhq/ui/tooltip';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import LabelTag from './LabelTag';
import styles from './LabelColumn.module.scss';
import { BADGE_GAP, estimateBadgeWidth, OVERFLOW_BADGE_WIDTH } from './utils';
export interface LabelColumnProps {
labels: string[];
color?:
| 'primary'
| 'secondary'
| 'success'
| 'error'
| 'warning'
| 'robin'
| 'forest'
| 'amber'
| 'sienna'
| 'cherry'
| 'sakura'
| 'aqua'
| 'vanilla';
value?: { [key: string]: string };
}
function LabelColumn({
labels,
value,
color = 'primary',
}: LabelColumnProps): JSX.Element {
const containerRef = useRef<HTMLDivElement>(null);
const [maxVisibleCount, setMaxVisibleCount] = useState(labels.length);
const [, copyToClipboard] = useCopyToClipboard();
const calculateMaxVisible = useCallback(
(width: number): number => {
if (width <= 0) {
return 1;
}
const availableWidth = width - OVERFLOW_BADGE_WIDTH - BADGE_GAP;
let usedWidth = 0;
let count = 0;
for (const label of labels) {
const badgeWidth = estimateBadgeWidth(label, value?.[label]) + BADGE_GAP;
if (usedWidth + badgeWidth > availableWidth && count > 0) {
break;
}
usedWidth += badgeWidth;
count++;
}
return Math.max(1, count);
},
[labels, value],
);
useEffect(() => {
const container = containerRef.current;
if (!container) {
return;
}
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0) {
setMaxVisibleCount(calculateMaxVisible(entry.contentRect.width));
}
});
observer.observe(container);
if (container.clientWidth > 0) {
setMaxVisibleCount(calculateMaxVisible(container.clientWidth));
}
return (): void => observer.disconnect();
}, [calculateMaxVisible]);
const needsOverflow = labels.length > maxVisibleCount;
const visibleLabels = needsOverflow
? labels.slice(0, maxVisibleCount)
: labels;
const remainingLabels = needsOverflow ? labels.slice(maxVisibleCount) : [];
return (
<div
ref={containerRef}
className={styles.labelColumn}
data-testid="label-column"
>
{visibleLabels.map((label) => (
<LabelTag key={label} label={label} color={color} value={value?.[label]} />
))}
{remainingLabels.length > 0 && (
<TooltipRoot>
<TooltipTrigger asChild>
<span>
<Badge
color={color}
className={styles.overflowBadge}
variant="outline"
data-testid="label-overflow-badge"
>
+{remainingLabels.length}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent side="bottom" align="end">
<div className={styles.tooltipContent}>
<span>
{remainingLabels
.map((label) => (value?.[label] ? `${label}: ${value[label]}` : label))
.join(', ')}
</span>
<button
type="button"
className={styles.copyButton}
onClick={(e): void => {
e.stopPropagation();
const searchFormat = remainingLabels
.map((label) => (value?.[label] ? `${label} ${value[label]}` : label))
.join(' ');
copyToClipboard(searchFormat);
toast.success('Copied! Use in search to filter alerts.');
}}
aria-label="Copy to clipboard"
>
<Copy size={12} />
</button>
</div>
</TooltipContent>
</TooltipRoot>
)}
</div>
);
}
export default LabelColumn;

View File

@@ -1,30 +0,0 @@
.labelBadge {
cursor: default;
font-size: 12px;
max-width: 180px;
text-overflow: ellipsis;
}
.labelValue {
text-overflow: ellipsis;
overflow: hidden;
}
.tooltipContent {
display: flex;
align-items: center;
gap: 8px;
}
.copyButton {
all: unset;
cursor: pointer;
display: flex;
align-items: center;
opacity: 0.7;
&:hover {
opacity: 1;
}
}

View File

@@ -1,74 +0,0 @@
import { Copy } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { toast } from '@signozhq/ui/sonner';
import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from '@signozhq/ui/tooltip';
import { useCopyToClipboard } from 'react-use';
import styles from './LabelTag.module.scss';
export interface LabelTagProps {
label: string;
color?:
| 'primary'
| 'secondary'
| 'success'
| 'error'
| 'warning'
| 'robin'
| 'forest'
| 'amber'
| 'sienna'
| 'cherry'
| 'sakura'
| 'aqua'
| 'vanilla';
value?: string;
}
function LabelTag({ label, value, color }: LabelTagProps): JSX.Element {
const [, copyToClipboard] = useCopyToClipboard();
const displayText = value ? `${label}: ${value}` : label;
const searchFormat = value ? `${label} ${value}` : label;
const handleCopy = (e: React.MouseEvent): void => {
e.stopPropagation();
copyToClipboard(searchFormat);
toast.success('Copied! Use in search to filter alerts.');
};
return (
<TooltipRoot>
<TooltipTrigger asChild>
<span>
<Badge
color={color}
className={styles.labelBadge}
variant="outline"
data-testid={`label-tag-${label}`}
>
<span className={styles.labelValue}>{displayText}</span>
</Badge>
</span>
</TooltipTrigger>
<TooltipContent>
<div className={styles.tooltipContent}>
<span>{displayText}</span>
<button
type="button"
className={styles.copyButton}
onClick={handleCopy}
aria-label="Copy to clipboard"
>
<Copy size={12} />
</button>
</div>
</TooltipContent>
</TooltipRoot>
);
}
export default LabelTag;

View File

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

View File

@@ -1,14 +0,0 @@
export const BADGE_GAP = 4;
export const OVERFLOW_BADGE_WIDTH = 40;
export const BADGE_MAX_WIDTH = 180;
export const BADGE_PADDING = 16;
export const CHAR_WIDTH = 7;
export function estimateBadgeWidth(label: string, value?: string): number {
const displayText = value ? `${label}: ${value}` : label;
return Math.min(
displayText.length * CHAR_WIDTH + BADGE_PADDING,
BADGE_MAX_WIDTH,
);
}

View File

@@ -1,30 +0,0 @@
.emptyState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 48px 24px;
text-align: center;
}
.icon {
color: var(--text-vanilla-400);
}
.title {
font-size: 16px;
font-weight: 500;
color: var(--text-vanilla-100);
}
.subtitle {
font-size: 14px;
color: var(--text-vanilla-400);
max-width: 400px;
}
.actions {
display: flex;
gap: 8px;
}

View File

@@ -1,71 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import NoResultsEmptyState from './NoResultsEmptyState';
describe('NoResultsEmptyState', () => {
it('should render with default props', () => {
render(<NoResultsEmptyState />);
expect(screen.getByTestId('no-results-empty-state')).toBeInTheDocument();
expect(screen.getByTestId('no-results-title')).toHaveTextContent(
'No matching results',
);
expect(screen.getByTestId('no-results-subtitle')).toHaveTextContent(
'No items match your current filters. Try adjusting your search criteria.',
);
});
it('should render with custom title and subtitle', () => {
render(
<NoResultsEmptyState title="Custom Title" subtitle="Custom Subtitle" />,
);
expect(screen.getByTestId('no-results-title')).toHaveTextContent(
'Custom Title',
);
expect(screen.getByTestId('no-results-subtitle')).toHaveTextContent(
'Custom Subtitle',
);
});
it('should not render clear button when onClear is not provided', () => {
render(<NoResultsEmptyState />);
expect(
screen.queryByTestId('no-results-clear-button'),
).not.toBeInTheDocument();
});
it('should render clear button when onClear is provided', () => {
const onClear = jest.fn();
render(<NoResultsEmptyState onClear={onClear} />);
expect(screen.getByTestId('no-results-clear-button')).toBeInTheDocument();
expect(screen.getByTestId('no-results-clear-button')).toHaveTextContent(
'Clear Filters',
);
});
it('should render custom clear button text', () => {
render(
<NoResultsEmptyState onClear={jest.fn()} clearButtonText="Reset All" />,
);
expect(screen.getByTestId('no-results-clear-button')).toHaveTextContent(
'Reset All',
);
});
it('should call onClear when clear button is clicked', async () => {
const user = userEvent.setup();
const onClear = jest.fn();
render(<NoResultsEmptyState onClear={onClear} />);
await user.click(screen.getByTestId('no-results-clear-button'));
expect(onClear).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,57 +0,0 @@
import { RefreshCw, Search } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import styles from './NoResultsEmptyState.module.scss';
interface NoResultsEmptyStateProps {
title?: string;
subtitle?: string;
onClear?: () => void;
clearButtonText?: string;
onRefresh?: () => void;
}
function NoResultsEmptyState({
title = 'No matching results',
subtitle = 'No items match your current filters. Try adjusting your search criteria.',
onClear,
clearButtonText = 'Clear Filters',
onRefresh,
}: NoResultsEmptyStateProps): JSX.Element {
return (
<div className={styles.emptyState} data-testid="no-results-empty-state">
<Search className={styles.icon} size={16} />
<div className={styles.title} data-testid="no-results-title">
{title}
</div>
<div className={styles.subtitle} data-testid="no-results-subtitle">
{subtitle}
</div>
<div className={styles.actions}>
{onClear && (
<Button
variant="outlined"
color="secondary"
onClick={onClear}
data-testid="no-results-clear-button"
>
{clearButtonText}
</Button>
)}
{onRefresh && (
<Button
variant="outlined"
color="secondary"
prefix={<RefreshCw size={14} />}
onClick={onRefresh}
data-testid="no-results-refresh-button"
>
Refresh
</Button>
)}
</div>
</div>
);
}
export default NoResultsEmptyState;

View File

@@ -1 +0,0 @@
export { default } from './NoResultsEmptyState';

View File

@@ -1,32 +0,0 @@
import type { BadgeColor } from '@signozhq/ui/badge';
export const STATE_ORDER = ['firing', 'pending', 'inactive', 'disabled'];
export const SEVERITY_ORDER = ['critical', 'error', 'warning', 'info'];
export const STATE_LABELS: Record<string, string> = {
firing: 'Firing',
pending: 'Pending',
inactive: 'OK',
disabled: 'Disabled',
};
export const STATE_COLORS: Record<string, string> = {
firing: 'var(--bg-cherry-500)',
pending: 'var(--bg-amber-500)',
inactive: 'var(--bg-forest-500)',
disabled: 'var(--l2-foreground)',
};
export const SEVERITY_COLORS: Record<string, string> = {
critical: 'var(--bg-cherry-500)',
error: 'var(--bg-cherry-400)',
warning: 'var(--bg-amber-500)',
info: 'var(--bg-robin-500)',
};
export const SEVERITY_BADGE_COLORS: Record<string, BadgeColor> = {
critical: 'error',
error: 'error',
warning: 'warning',
info: 'primary',
};

View File

@@ -1,7 +0,0 @@
export interface FilterValue {
value: string;
}
export interface AlertWithLabels {
labels?: Record<string, string>;
}

View File

@@ -1,287 +0,0 @@
import type { SortState } from 'components/TanStackTableView/types';
import type { AlertWithLabels, FilterValue } from './types';
import { filterByLabels, searchByLabels, sortByColumn } from './utils';
interface TestAlert extends AlertWithLabels {
name: string;
value: number;
}
const createAlert = (
name: string,
value: number,
labels?: Record<string, string>,
): TestAlert => ({
name,
value,
labels,
});
describe('sortByColumn', () => {
const alerts: TestAlert[] = [
createAlert('Alert C', 3),
createAlert('Alert A', 1),
createAlert('Alert B', 2),
];
const getSortValue = (
item: TestAlert,
columnName: string,
): string | number => {
if (columnName === 'name') {
return item.name;
}
if (columnName === 'value') {
return item.value;
}
return '';
};
it('should return items unchanged when no orderBy provided', () => {
const result = sortByColumn(alerts, null, getSortValue);
expect(result).toStrictEqual(alerts);
});
it('should sort by string column ascending', () => {
const orderBy: SortState = { columnName: 'name', order: 'asc' };
const result = sortByColumn(alerts, orderBy, getSortValue);
expect(result.map((a) => a.name)).toStrictEqual([
'Alert A',
'Alert B',
'Alert C',
]);
});
it('should sort by string column descending', () => {
const orderBy: SortState = { columnName: 'name', order: 'desc' };
const result = sortByColumn(alerts, orderBy, getSortValue);
expect(result.map((a) => a.name)).toStrictEqual([
'Alert C',
'Alert B',
'Alert A',
]);
});
it('should sort by number column ascending', () => {
const orderBy: SortState = { columnName: 'value', order: 'asc' };
const result = sortByColumn(alerts, orderBy, getSortValue);
expect(result.map((a) => a.value)).toStrictEqual([1, 2, 3]);
});
it('should sort by number column descending', () => {
const orderBy: SortState = { columnName: 'value', order: 'desc' };
const result = sortByColumn(alerts, orderBy, getSortValue);
expect(result.map((a) => a.value)).toStrictEqual([3, 2, 1]);
});
it('should use defaultSort when orderBy is null', () => {
const defaultSort: SortState = { columnName: 'value', order: 'asc' };
const result = sortByColumn(alerts, null, getSortValue, defaultSort);
expect(result.map((a) => a.value)).toStrictEqual([1, 2, 3]);
});
it('should not mutate original array', () => {
const original = [...alerts];
const orderBy: SortState = { columnName: 'name', order: 'asc' };
sortByColumn(alerts, orderBy, getSortValue);
expect(alerts).toStrictEqual(original);
});
it('should handle empty array', () => {
const result = sortByColumn(
[],
{ columnName: 'name', order: 'asc' },
getSortValue,
);
expect(result).toStrictEqual([]);
});
it('should handle equal values', () => {
const duplicates = [
createAlert('Same', 1),
createAlert('Same', 1),
createAlert('Same', 1),
];
const orderBy: SortState = { columnName: 'name', order: 'asc' };
const result = sortByColumn(duplicates, orderBy, getSortValue);
expect(result).toHaveLength(3);
});
});
describe('searchByLabels', () => {
const alerts: TestAlert[] = [
createAlert('CPU High', 1, { severity: 'critical', team: 'infra' }),
createAlert('Memory Warning', 2, { severity: 'warning', team: 'backend' }),
createAlert('Disk Full', 3, { severity: 'error', region: 'us-east' }),
createAlert('Network Slow', 4, {}),
createAlert('No Labels', 5),
];
const getAlertName = (alert: TestAlert): string => alert.name;
it('should return all items when search is empty', () => {
const result = searchByLabels(alerts, '', getAlertName);
expect(result).toStrictEqual(alerts);
});
it('should return all items when search is whitespace', () => {
const result = searchByLabels(alerts, ' ', getAlertName);
expect(result).toStrictEqual(alerts);
});
it('should search by alert name', () => {
const result = searchByLabels(alerts, 'CPU', getAlertName);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('CPU High');
});
it('should search by alert name case-insensitive', () => {
const result = searchByLabels(alerts, 'cpu', getAlertName);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('CPU High');
});
it('should search by severity label', () => {
const result = searchByLabels(alerts, 'critical', getAlertName);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('CPU High');
});
it('should search by any label key', () => {
const result = searchByLabels(alerts, 'team', getAlertName);
expect(result).toHaveLength(2);
});
it('should search by any label value', () => {
const result = searchByLabels(alerts, 'infra', getAlertName);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('CPU High');
});
it('should handle alerts with no labels', () => {
const result = searchByLabels(alerts, 'No Labels', getAlertName);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('No Labels');
});
it('should handle partial matches', () => {
const result = searchByLabels(alerts, 'warn', getAlertName);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('Memory Warning');
});
it('should return empty for no matches', () => {
const result = searchByLabels(alerts, 'nonexistent', getAlertName);
expect(result).toStrictEqual([]);
});
it('should trim search text', () => {
const result = searchByLabels(alerts, ' CPU ', getAlertName);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('CPU High');
});
});
describe('filterByLabels', () => {
const alerts: TestAlert[] = [
createAlert('A1', 1, { severity: 'critical', team: 'infra', env: 'prod' }),
createAlert('A2', 2, { severity: 'critical', team: 'backend', env: 'prod' }),
createAlert('A3', 3, { severity: 'warning', team: 'infra', env: 'staging' }),
createAlert('A4', 4, { severity: 'info', team: 'frontend', env: 'dev' }),
createAlert('A5', 5, {}),
createAlert('A6', 6),
];
const createFilter = (value: string): FilterValue => ({ value });
it('should return all items when filters are empty', () => {
const result = filterByLabels(alerts, []);
expect(result).toStrictEqual(alerts);
});
it('should return all items when filters is null-ish', () => {
const result = filterByLabels(alerts, null as unknown as FilterValue[]);
expect(result).toStrictEqual(alerts);
});
it('should filter by single label', () => {
const filters = [createFilter('severity:critical')];
const result = filterByLabels(alerts, filters);
expect(result).toHaveLength(2);
expect(result.map((a) => a.name)).toStrictEqual(['A1', 'A2']);
});
it('should use OR logic for same key', () => {
const filters = [
createFilter('severity:critical'),
createFilter('severity:warning'),
];
const result = filterByLabels(alerts, filters);
expect(result).toHaveLength(3);
expect(result.map((a) => a.name)).toStrictEqual(['A1', 'A2', 'A3']);
});
it('should use AND logic for different keys', () => {
const filters = [
createFilter('severity:critical'),
createFilter('team:infra'),
];
const result = filterByLabels(alerts, filters);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('A1');
});
it('should handle case-insensitive keys', () => {
const filters = [createFilter('SEVERITY:critical')];
const result = filterByLabels(alerts, filters);
expect(result).toHaveLength(2);
});
it('should handle case-insensitive values', () => {
const filters = [createFilter('severity:CRITICAL')];
const result = filterByLabels(alerts, filters);
expect(result).toHaveLength(2);
});
it('should trim whitespace', () => {
const filters = [createFilter(' severity : critical ')];
const result = filterByLabels(alerts, filters);
expect(result).toHaveLength(2);
});
it('should return empty for invalid filter format', () => {
const filters = [createFilter('invalid')];
const result = filterByLabels(alerts, filters);
expect(result).toStrictEqual([]);
});
it('should ignore invalid filters mixed with valid', () => {
const filters = [createFilter('invalid'), createFilter('severity:critical')];
const result = filterByLabels(alerts, filters);
expect(result).toHaveLength(2);
});
it('should exclude alerts without matching label key', () => {
const filters = [createFilter('nonexistent:value')];
const result = filterByLabels(alerts, filters);
expect(result).toStrictEqual([]);
});
it('should exclude alerts with no labels', () => {
const filters = [createFilter('severity:critical')];
const result = filterByLabels(alerts, filters);
expect(result.every((a) => a.labels !== undefined)).toBe(true);
});
it('should handle complex AND/OR combinations', () => {
const filters = [
createFilter('env:prod'),
createFilter('env:staging'),
createFilter('team:infra'),
];
const result = filterByLabels(alerts, filters);
expect(result).toHaveLength(2);
expect(result.map((a) => a.name)).toStrictEqual(['A1', 'A3']);
});
});

View File

@@ -1,116 +0,0 @@
import type { SortState } from 'components/TanStackTableView/types';
import type { AlertWithLabels, FilterValue } from './types';
/**
* Generic sort function for alert-like data
*/
export function sortByColumn<T>(
items: T[],
orderBy: SortState | null,
getSortValue: (item: T, columnName: string) => string | number,
defaultSort?: SortState,
): T[] {
const sortState = orderBy ?? defaultSort;
if (!sortState) {
return items;
}
const { columnName, order } = sortState;
const multiplier = order === 'asc' ? 1 : -1;
return [...items].sort((a, b) => {
const aVal = getSortValue(a, columnName);
const bVal = getSortValue(b, columnName);
if (aVal < bVal) {
return -1 * multiplier;
}
if (aVal > bVal) {
return 1 * multiplier;
}
return 0;
});
}
/**
* Search alerts/rules by name, severity, and all labels
*/
export function searchByLabels<T extends AlertWithLabels>(
items: T[],
searchText: string,
getAlertName: (item: T) => string,
): T[] {
if (!searchText.trim()) {
return items;
}
const value = searchText.toLowerCase().trim();
return items.filter((item) => {
const alertName = getAlertName(item).toLowerCase();
const severity = item.labels?.severity?.toLowerCase() ?? '';
const labelSearchString = Object.entries(item.labels ?? {})
.map(([key, val]) => `${key} ${val}`)
.join(' ')
.toLowerCase();
return (
alertName.includes(value) ||
severity.includes(value) ||
labelSearchString.includes(value)
);
});
}
/**
* Filter alerts by label key:value pairs
* Same key uses OR logic, different keys use AND logic
*/
export function filterByLabels<T extends AlertWithLabels>(
items: T[],
selectedFilters: FilterValue[],
): T[] {
if (!selectedFilters?.length) {
return items;
}
const validFilters = selectedFilters
.map((e) => e.value)
.filter((v) => v.split(':').length === 2);
if (!validFilters.length) {
return [];
}
// Group values by key - same key uses OR, different keys use AND
const filtersByKey = new Map<string, string[]>();
validFilters.forEach((f) => {
const [key, value] = f.split(':');
const trimmedKey = key.trim().toLowerCase();
const trimmedValue = value.trim().toLowerCase();
const existing = filtersByKey.get(trimmedKey) ?? [];
existing.push(trimmedValue);
filtersByKey.set(trimmedKey, existing);
});
return items.filter((item) => {
if (!item.labels) {
return false;
}
// All keys must match (AND), any value per key can match (OR)
return Array.from(filtersByKey.entries()).every(([filterKey, values]) => {
// Case-insensitive key lookup
const matchingKey = Object.keys(item.labels ?? {}).find(
(k) => k.toLowerCase() === filterKey,
);
if (!matchingKey) {
return false;
}
const labelValue = item.labels?.[matchingKey]?.toLowerCase();
return values.some((v) => labelValue === v);
});
});
}

View File

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

View File

@@ -9,7 +9,7 @@ import {
useState,
} from 'react';
import { Color } from '@signozhq/design-tokens';
import { Select, Tooltip } from 'antd';
import { Select, Tag, Tooltip } from 'antd';
import {
OPERATORS,
QUERY_BUILDER_OPERATORS_BY_TYPES,
@@ -51,7 +51,6 @@ import { popupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
import './ClientSideQBSearch.styles.scss';
import { Badge } from '@signozhq/ui/badge';
export interface AttributeKey {
key: string;
@@ -548,14 +547,10 @@ function ClientSideQBSearch(
return (
<span className="qb-search-bar-tokenised-tags">
<Badge
color="vanilla"
className={tagDetails?.key?.type || ''}
<Tag
closable={!searchValue && closable}
onClose={(e): void => {
e.preventDefault();
onCloseHandler();
}}
onClose={onCloseHandler}
className={tagDetails?.key?.type || ''}
>
<Tooltip title={chipValue}>
<TypographyText
@@ -571,7 +566,7 @@ function ClientSideQBSearch(
{chipValue}
</TypographyText>
</Tooltip>
</Badge>
</Tag>
</span>
);
};

View File

@@ -41,22 +41,14 @@ $item-spacing: 8px;
width: 100%;
background: transparent;
border: none;
border-radius: 0;
box-shadow: none;
outline: none;
height: auto;
color: var(--l1-foreground);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
padding: 0;
&:focus,
&:focus-visible,
&:hover {
border: none;
&.ant-input:focus {
box-shadow: none;
outline: none;
}
&::placeholder {

View File

@@ -6,7 +6,7 @@ import {
useState,
} from 'react';
import { Color } from '@signozhq/design-tokens';
import { Input } from '@signozhq/ui/input';
import { Input } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts';

View File

@@ -1,6 +1,5 @@
import { useCallback, useMemo, useState } from 'react';
import { Button, Popover, Tooltip } from 'antd';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { Button, Popover, Radio, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { TelemetryFieldKey } from 'api/v5/v5';
import { useExportRawData } from 'hooks/useDownloadOptionsMenu/useDownloadOptionsMenu';
@@ -64,30 +63,27 @@ export default function DownloadOptionsMenu({
>
<div className="export-format">
<Typography.Text className="title">FORMAT</Typography.Text>
<RadioGroup value={exportFormat} onChange={setExportFormat}>
<RadioGroupItem value={DownloadFormats.CSV}>csv</RadioGroupItem>
<RadioGroupItem value={DownloadFormats.JSONL}>jsonl</RadioGroupItem>
</RadioGroup>
<Radio.Group
value={exportFormat}
onChange={(e): void => setExportFormat(e.target.value)}
>
<Radio value={DownloadFormats.CSV}>csv</Radio>
<Radio value={DownloadFormats.JSONL}>jsonl</Radio>
</Radio.Group>
</div>
<div className="horizontal-line" />
<div className="row-limit">
<Typography.Text className="title">Number of Rows</Typography.Text>
<RadioGroup
value={String(rowLimit)}
onChange={(value): void => setRowLimit(Number(value))}
<Radio.Group
value={rowLimit}
onChange={(e): void => setRowLimit(e.target.value)}
>
<RadioGroupItem value={String(DownloadRowCounts.TEN_K)}>
10k
</RadioGroupItem>
<RadioGroupItem value={String(DownloadRowCounts.THIRTY_K)}>
30k
</RadioGroupItem>
<RadioGroupItem value={String(DownloadRowCounts.FIFTY_K)}>
50k
</RadioGroupItem>
</RadioGroup>
<Radio value={DownloadRowCounts.TEN_K}>10k</Radio>
<Radio value={DownloadRowCounts.THIRTY_K}>30k</Radio>
<Radio value={DownloadRowCounts.FIFTY_K}>50k</Radio>
</Radio.Group>
</div>
{dataSource !== DataSource.TRACES && (
@@ -96,12 +92,13 @@ export default function DownloadOptionsMenu({
<div className="columns-scope">
<Typography.Text className="title">Columns</Typography.Text>
<RadioGroup value={columnsScope} onChange={setColumnsScope}>
<RadioGroupItem value={DownloadColumnsScopes.ALL}>All</RadioGroupItem>
<RadioGroupItem value={DownloadColumnsScopes.SELECTED}>
Selected
</RadioGroupItem>
</RadioGroup>
<Radio.Group
value={columnsScope}
onChange={(e): void => setColumnsScope(e.target.value)}
>
<Radio value={DownloadColumnsScopes.ALL}>All</Radio>
<Radio value={DownloadColumnsScopes.SELECTED}>Selected</Radio>
</Radio.Group>
</div>
</>
)}

View File

@@ -0,0 +1,7 @@
.dropdown-button {
color: var(--l1-foreground);
}
.dropdown-icon {
font-size: 1.2rem;
}

View File

@@ -0,0 +1,51 @@
import { useState } from 'react';
import { Ellipsis } from '@signozhq/icons';
import { Button, Dropdown, MenuProps } from 'antd';
import './DropDown.styles.scss';
function DropDown({
element,
onDropDownItemClick,
}: {
element: JSX.Element[];
onDropDownItemClick?: MenuProps['onClick'];
}): JSX.Element {
const items: MenuProps['items'] = element.map(
(e: JSX.Element, index: number) => ({
label: e,
key: index,
}),
);
const [isDdOpen, setDdOpen] = useState<boolean>(false);
return (
<Dropdown
menu={{
items,
onMouseEnter: (): void => setDdOpen(true),
onMouseLeave: (): void => setDdOpen(false),
onClick: (item): void => onDropDownItemClick?.(item),
}}
open={isDdOpen}
>
<Button
type="link"
className={`dropdown-button`}
onClick={(e): void => {
e.preventDefault();
setDdOpen(true);
}}
>
<Ellipsis className="dropdown-icon" size={16} />
</Button>
</Dropdown>
);
}
DropDown.defaultProps = {
onDropDownItemClick: (): void => {},
};
export default DropDown;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Color } from '@signozhq/design-tokens';
import { Button, Modal } from 'antd';
import { Button, Modal, Tag } from 'antd';
import { CircleAlert, X } from '@signozhq/icons';
import KeyValueLabel from 'periscope/components/KeyValueLabel';
import { useAppContext } from 'providers/App/App';
@@ -9,7 +9,6 @@ import APIError from 'types/api/error';
import ErrorContent from './components/ErrorContent';
import './ErrorModal.styles.scss';
import { Badge } from '@signozhq/ui/badge';
type Props = {
error: APIError;
@@ -46,17 +45,14 @@ function ErrorModal({
return (
<>
{!triggerComponent ? (
<span
<Tag
className="error-modal__trigger"
role="button"
tabIndex={0}
icon={<CircleAlert size={14} color={Color.BG_CHERRY_500} />}
color="error"
onClick={(): void => setVisible(true)}
onKeyDown={undefined}
>
<Badge color="error">
<CircleAlert size={14} color={Color.BG_CHERRY_500} /> error
</Badge>
</span>
error
</Tag>
) : (
React.cloneElement(triggerComponent, {
onClick: () => setVisible(true),

View File

@@ -1,7 +1,15 @@
import { useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Button, Col, Popover, Row, Select, Space } from 'antd';
import { DropdownMenuSimple, type MenuProps } from '@signozhq/ui/dropdown-menu';
import {
Button,
Col,
Dropdown,
MenuProps,
Popover,
Row,
Select,
Space,
} from 'antd';
import { Typography } from '@signozhq/ui/typography';
import axios from 'axios';
import TextToolTip from 'components/TextToolTip';
@@ -233,9 +241,9 @@ function ExplorerCard({
</Popover>
<Share2 onClick={onCopyUrlHandler} size="md" />
{viewKey && (
<DropdownMenuSimple menu={moreOptionMenu}>
<Button type="text" size="small" icon={<Ellipsis size="md" />} />
</DropdownMenuSimple>
<Dropdown trigger={['click']} menu={moreOptionMenu}>
<Ellipsis size="md" />
</Dropdown>
)}
</Space>
</OffSetCol>

View File

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

View File

@@ -27,12 +27,10 @@ function SortableField({
field,
onRemove,
allowDrag,
isRequired,
}: {
field: TelemetryFieldKey;
onRemove: (field: TelemetryFieldKey) => void;
allowDrag: boolean;
isRequired: boolean;
}): JSX.Element {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: field.name });
@@ -55,17 +53,15 @@ function SortableField({
{allowDrag && <GripVertical size={14} />}
<span className={styles.fieldKey}>{field.name}</span>
</div>
{!isRequired && (
<Button
className={cx(styles.removeBtn, 'periscope-btn')}
variant="outlined"
color="destructive"
size="sm"
onClick={(): void => onRemove(field)}
>
Remove
</Button>
)}
<Button
className={cx(styles.removeBtn, 'periscope-btn')}
variant="outlined"
color="destructive"
size="sm"
onClick={(): void => onRemove(field)}
>
Remove
</Button>
</div>
);
}
@@ -75,7 +71,6 @@ interface AddedFieldsProps {
fields: TelemetryFieldKey[];
onFieldsChange: (fields: TelemetryFieldKey[]) => void;
maxFields?: number;
requiredFields?: readonly string[];
}
function AddedFields({
@@ -83,7 +78,6 @@ function AddedFields({
fields,
onFieldsChange,
maxFields,
requiredFields = [],
}: AddedFieldsProps): JSX.Element {
const sensors = useSensors(useSensor(PointerSensor));
@@ -141,7 +135,6 @@ function AddedFields({
field={field}
onRemove={handleRemove}
allowDrag={allowDrag}
isRequired={requiredFields.includes(field.name)}
/>
))}
</SortableContext>

View File

@@ -76,8 +76,11 @@
min-height: 0;
overflow: hidden;
// Ant Skeleton.Input rendered inside the loading state — override its
// hard-coded width.
:global(.ant-skeleton-input) {
width: 50% !important;
width: 300px;
margin: 8px 12px;
}
}
@@ -92,8 +95,7 @@
display: flex;
align-items: center;
justify-content: space-between;
height: 36px;
padding: 0 12px;
padding: 6px 12px;
border-radius: 4px;
user-select: none;
font-size: 13px;
@@ -130,7 +132,7 @@
}
.isDragDisabled {
cursor: default;
padding: 6px 12px;
}
.otherFieldItem {

View File

@@ -26,7 +26,6 @@ interface FieldsSelectorProps {
onClose: () => void;
signal: DataSource;
maxFields?: number;
requiredFields?: readonly string[];
width?: number;
height?: number;
defaultPosition?: { x: number; y: number };
@@ -40,7 +39,6 @@ function FieldsSelector({
onClose,
signal,
maxFields,
requiredFields,
width = DEFAULT_PANEL_WIDTH,
height,
defaultPosition,
@@ -140,7 +138,6 @@ function FieldsSelector({
fields={draftFields}
onFieldsChange={setDraftFields}
maxFields={maxFields}
requiredFields={requiredFields}
/>
<OtherFields

View File

@@ -64,13 +64,8 @@ function OtherFields({
<div className={styles.sectionHeader}>OTHER FIELDS</div>
<div className={styles.otherList}>
{Array.from({ length: 5 }).map((_, i) => (
<div
// eslint-disable-next-line react/no-array-index-key
key={i}
className={cx(styles.fieldItem, styles.otherFieldItem)}
>
<Skeleton.Input active size="small" block />
</div>
// eslint-disable-next-line react/no-array-index-key
<Skeleton.Input active size="small" key={i} />
))}
</div>
</div>

View File

@@ -1,73 +0,0 @@
import { render, screen } from 'tests/test-utils';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import AddedFields from '../AddedFields';
const makeField = (name: string): TelemetryFieldKey => ({
name,
signal: 'logs',
fieldContext: 'log',
fieldDataType: 'string',
});
describe('AddedFields — requiredFields', () => {
it('renders a Remove button for every field when no requiredFields are passed', () => {
const fields = [makeField('a'), makeField('b'), makeField('c')];
render(
<AddedFields inputValue="" fields={fields} onFieldsChange={jest.fn()} />,
);
expect(screen.getAllByRole('button', { name: /remove/i })).toHaveLength(3);
});
it('hides the Remove button for fields listed in requiredFields', () => {
const fields = [makeField('a'), makeField('b'), makeField('c')];
render(
<AddedFields
inputValue=""
fields={fields}
onFieldsChange={jest.fn()}
requiredFields={['a', 'c']}
/>,
);
// Only 'b' is removable.
const removeButtons = screen.getAllByRole('button', { name: /remove/i });
expect(removeButtons).toHaveLength(1);
});
it('still renders the field name + drag handle for required fields', () => {
const fields = [makeField('a'), makeField('b')];
render(
<AddedFields
inputValue=""
fields={fields}
onFieldsChange={jest.fn()}
requiredFields={['a']}
/>,
);
// Both names visible regardless of required status.
expect(screen.getByText('a')).toBeInTheDocument();
expect(screen.getByText('b')).toBeInTheDocument();
});
it('treats requiredFields as an exact-name match (substring matches do not lock)', () => {
const fields = [makeField('body'), makeField('body_extra')];
render(
<AddedFields
inputValue=""
fields={fields}
onFieldsChange={jest.fn()}
requiredFields={['body']}
/>,
);
// 'body' locked, 'body_extra' removable.
expect(screen.getAllByRole('button', { name: /remove/i })).toHaveLength(1);
});
});

View File

@@ -1,8 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { toast } from '@signozhq/ui/sonner';
import { Button, Input } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Button, Input, Radio, RadioChangeEvent } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { handleContactSupport } from 'container/Integrations/utils';
@@ -102,12 +101,13 @@ function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
return (
<div className="feedback-modal-container">
<div className="feedback-modal-header">
<ToggleGroupSimple
type="single"
<Radio.Group
value={activeTab}
defaultValue={activeTab}
optionType="button"
className="feedback-modal-tabs"
onChange={setActiveTab}
items={items}
options={items}
onChange={(e: RadioChangeEvent): void => setActiveTab(e.target.value)}
/>
</div>
<div className="feedback-modal-content">

View File

@@ -1,9 +1,8 @@
import { useCallback, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Dot } from '@signozhq/icons';
import { Dot, Sparkles } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import Noz from 'components/Noz/Noz';
import { Popover } from 'antd';
import logEvent from 'api/common/logEvent';
import { AIAssistantEvents } from 'container/AIAssistant/events';
@@ -22,7 +21,6 @@ import FeedbackModal from './FeedbackModal';
import ShareURLModal from './ShareURLModal';
import './HeaderRightSection.styles.scss';
import { Typography } from '@signozhq/ui/typography';
interface HeaderRightSectionProps {
enableAnnouncements: boolean;
@@ -109,22 +107,21 @@ function HeaderRightSection({
</span>
) : null}
<TooltipSimple title="Noz">
<TooltipSimple title="AI Assistant">
<Button
variant="solid"
color="secondary"
className="noz-wave"
onClick={handleOpenAIAssistant}
aria-label={
showHeaderPendingBadge
? pendingUserInputCount === 1
? 'Open Noz, 1 action needs your response'
: `Open Noz, ${pendingUserInputCount} actions need your response`
: 'Open Noz'
? 'Open AI Assistant, 1 action needs your response'
: `Open AI Assistant, ${pendingUserInputCount} actions need your response`
: 'Open AI Assistant'
}
prefix={<Noz size={20} />}
prefix={<Sparkles size={14} color="var(--primary)" />}
>
<Typography.Text>Noz</Typography.Text>
AI Assistant
</Button>
</TooltipSimple>
</div>

View File

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

View File

@@ -158,23 +158,23 @@
font-weight: var(--font-weight-normal);
}
> button {
.tab {
border: 1px solid var(--l1-border);
width: 114px;
}
&::before {
background: var(--l1-border);
}
.tab::before {
background: var(--l1-border);
}
&[data-state='on'] {
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--l1-border);
.selected_view {
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--l1-border);
}
&::before {
background: var(--l1-border);
}
}
.selected_view::before {
background: var(--l1-border);
}
}

View File

@@ -4,10 +4,9 @@ import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useCopyToClipboard, useLocation } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { Drawer, Tooltip } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Divider } from '@signozhq/ui/divider';
import { Divider, Drawer, Radio, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import type { RadioChangeEvent } from 'antd/lib';
import cx from 'classnames';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
@@ -198,8 +197,8 @@ function LogDetailInner({
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
const handleModeChange = (value: string): void => {
setSelectedView(value as VIEWS);
const handleModeChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setIsEdit(false);
setIsFilterVisible(false);
};
@@ -453,50 +452,56 @@ function LogDetailInner({
</div>
<div className="tabs-and-search">
<ToggleGroupSimple
type="single"
<Radio.Group
className="views-tabs"
onChange={handleModeChange}
value={selectedView}
items={[
{
value: VIEW_TYPES.OVERVIEW,
label: (
<div className="view-title">
<Table size={14} />
Overview
</div>
),
},
{
value: VIEW_TYPES.JSON,
label: (
<div className="view-title">
<Braces size={14} />
JSON
</div>
),
},
{
value: VIEW_TYPES.CONTEXT,
label: (
<div className="view-title">
<TextSelect size={14} />
Context
</div>
),
},
{
value: VIEW_TYPES.INFRAMETRICS,
label: (
<div className="view-title">
<Histogram size="md" />
Metrics
</div>
),
},
]}
/>
>
<Radio.Button
className={
selectedView === VIEW_TYPES.OVERVIEW ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.OVERVIEW}
>
<div className="view-title">
<Table size={14} />
Overview
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.JSON ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.JSON}
>
<div className="view-title">
<Braces size={14} />
JSON
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.CONTEXT ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.CONTEXT}
>
<div className="view-title">
<TextSelect size={14} />
Context
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.INFRAMETRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.INFRAMETRICS}
>
<div className="view-title">
<Histogram size="md" />
Metrics
</div>
</Radio.Button>
</Radio.Group>
<div className="log-detail-drawer__actions">
{selectedView === VIEW_TYPES.CONTEXT && (

View File

@@ -1,103 +0,0 @@
import { renderHook } from '@testing-library/react';
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields';
import { useLogsTableColumns } from '../useLogsTableColumns';
jest.mock('providers/Timezone', () => ({
useTimezone: (): { formatTimezoneAdjustedTimestamp: jest.Mock } => ({
formatTimezoneAdjustedTimestamp: jest.fn(() => 'TS'),
}),
}));
const field = (name: string): IField => ({
name,
type: '',
dataType: 'string',
});
describe('useLogsTableColumns — selectColumns-order respected', () => {
it('prepends stateIndicator and renders user fields in array order', () => {
const { result } = renderHook(() =>
useLogsTableColumns({
fields: [field('c'), field('a'), field('b')],
fontSize: FontSize.SMALL,
}),
);
expect(result.current.map((c) => c.id)).toStrictEqual([
'state-indicator',
'c',
'a',
'b',
]);
});
it('slots body and timestamp at their position in the fields array', () => {
const { result } = renderHook(() =>
useLogsTableColumns({
fields: [
field('service.name'),
field('body'),
field('request.id'),
field('timestamp'),
],
fontSize: FontSize.SMALL,
}),
);
// body/timestamp are NOT pinned to fixed positions — they appear where the
// caller placed them in `fields`.
expect(result.current.map((c) => c.id)).toStrictEqual([
'state-indicator',
'service.name',
'body',
'request.id',
'timestamp',
]);
});
it('skips the synthetic "id" field name', () => {
const { result } = renderHook(() =>
useLogsTableColumns({
fields: [field('id'), field('a'), field('b')],
fontSize: FontSize.SMALL,
}),
);
expect(result.current.map((c) => c.id)).toStrictEqual([
'state-indicator',
'a',
'b',
]);
});
it('uses the special body/timestamp coldefs (canBeHidden=false), not the generic user field def', () => {
const { result } = renderHook(() =>
useLogsTableColumns({
fields: [field('body'), field('timestamp'), field('user_field')],
fontSize: FontSize.SMALL,
}),
);
const byId = new Map(result.current.map((c) => [c.id, c]));
// body + timestamp are locked from the table-X removal pathway.
expect(byId.get('body')?.canBeHidden).toBe(false);
expect(byId.get('body')?.enableRemove).toBe(false);
expect(byId.get('timestamp')?.canBeHidden).toBe(false);
expect(byId.get('timestamp')?.enableRemove).toBe(false);
// User-added fields stay removable.
expect(byId.get('user_field')?.enableRemove).toBe(true);
});
it('renders only the stateIndicator when fields is empty', () => {
const { result } = renderHook(() =>
useLogsTableColumns({
fields: [],
fontSize: FontSize.SMALL,
}),
);
expect(result.current.map((c) => c.id)).toStrictEqual(['state-indicator']);
});
});

View File

@@ -18,11 +18,13 @@ import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
type UseLogsTableColumnsProps = {
fields: IField[];
fontSize: FontSize;
appendTo?: 'center' | 'end';
};
export function useLogsTableColumns({
fields,
fontSize,
appendTo = 'center',
}: UseLogsTableColumnsProps): TableColumnDef<ILog>[] {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
@@ -45,71 +47,71 @@ export function useLogsTableColumns({
),
};
const timestampCol: TableColumnDef<ILog> = {
id: 'timestamp',
header: 'Timestamp',
accessorFn: (log): unknown => log.timestamp,
canBeHidden: false,
enableRemove: false,
width: { default: 170, min: 170 },
cell: ({ value }): ReactElement => {
const ts = value as string | number;
const formatted =
typeof ts === 'string'
? formatTimezoneAdjustedTimestamp(ts, DATE_TIME_FORMATS.ISO_DATETIME_MS)
: formatTimezoneAdjustedTimestamp(
ts / 1e6,
DATE_TIME_FORMATS.ISO_DATETIME_MS,
);
return <TanStackTable.Text>{formatted}</TanStackTable.Text>;
},
};
const fieldColumns: TableColumnDef<ILog>[] = fields
.filter((f): boolean => !['id', 'body', 'timestamp'].includes(f.name))
.map(
(f): TableColumnDef<ILog> => ({
id: f.name,
header: f.name,
accessorFn: (log): unknown => FlatLogData(log)[f.name],
enableRemove: true,
width: { min: 192 },
cell: ({ value }): ReactElement => (
<TanStackTable.Text>{String(value ?? '')}</TanStackTable.Text>
),
}),
);
const bodyCol: TableColumnDef<ILog> = {
id: 'body',
header: 'Body',
accessorFn: (log): string => getBodyDisplayString(log.body),
canBeHidden: false,
enableRemove: false,
width: { default: '100%', min: 300 },
cell: ({ value, isActive }): ReactElement => (
<TanStackTable.Text
dangerouslySetInnerHTML={{
__html: getSanitizedLogBody(value as string, {
shouldEscapeHtml: true,
}),
}}
data-active={isActive}
/>
),
};
const makeUserFieldCol = (f: IField): TableColumnDef<ILog> => ({
id: f.name,
header: f.name,
accessorFn: (log): unknown => FlatLogData(log)[f.name],
enableRemove: true,
width: { min: 192 },
cell: ({ value }): ReactElement => (
<TanStackTable.Text>{String(value ?? '')}</TanStackTable.Text>
),
});
const fieldCols = fields
.map((f): TableColumnDef<ILog> | null => {
if (f.name === 'id') {
return null;
const timestampCol: TableColumnDef<ILog> | null = fields.some(
(f) => f.name === 'timestamp',
)
? {
id: 'timestamp',
header: 'Timestamp',
accessorFn: (log): unknown => log.timestamp,
width: { default: 170, min: 170 },
cell: ({ value }): ReactElement => {
const ts = value as string | number;
const formatted =
typeof ts === 'string'
? formatTimezoneAdjustedTimestamp(ts, DATE_TIME_FORMATS.ISO_DATETIME_MS)
: formatTimezoneAdjustedTimestamp(
ts / 1e6,
DATE_TIME_FORMATS.ISO_DATETIME_MS,
);
return <TanStackTable.Text>{formatted}</TanStackTable.Text>;
},
}
if (f.name === 'timestamp') {
return timestampCol;
}
if (f.name === 'body') {
return bodyCol;
}
return makeUserFieldCol(f);
})
.filter((c): c is TableColumnDef<ILog> => c !== null);
: null;
return [stateIndicatorCol, ...fieldCols];
}, [fields, fontSize, formatTimezoneAdjustedTimestamp]);
const bodyCol: TableColumnDef<ILog> | null = fields.some(
(f) => f.name === 'body',
)
? {
id: 'body',
header: 'Body',
accessorFn: (log): string => getBodyDisplayString(log.body),
canBeHidden: false,
width: { default: '100%', min: 300 },
cell: ({ value, isActive }): ReactElement => (
<TanStackTable.Text
dangerouslySetInnerHTML={{
__html: getSanitizedLogBody(value as string, {
shouldEscapeHtml: true,
}),
}}
data-active={isActive}
/>
),
}
: null;
return [
stateIndicatorCol,
...(timestampCol ? [timestampCol] : []),
...(appendTo === 'center' ? fieldColumns : []),
...(bodyCol ? [bodyCol] : []),
...(appendTo === 'end' ? fieldColumns : []),
];
}, [fields, appendTo, fontSize, formatTimezoneAdjustedTimestamp]);
}

View File

@@ -256,34 +256,6 @@
}
}
.edit-columns-container {
padding: 12px;
.edit-columns-btn {
display: flex;
width: 100%;
height: 20px;
padding: 4px 0px;
justify-content: space-between;
align-items: center;
border: none !important;
.edit-columns-text {
color: var(--l2-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
}
}
.edit-columns-btn:hover {
background-color: unset !important;
}
}
.add-new-column-container {
display: flex;
flex-direction: column;

View File

@@ -1,9 +1,11 @@
import { useCallback, useEffect, useState } from 'react';
import { Button, InputNumber, Popover, Tooltip } from 'antd';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Input, InputNumber, Popover, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import type { DefaultOptionType } from 'antd/es/select';
import cx from 'classnames';
import { LogViewMode } from 'container/LogsTable';
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import {
Check,
ChevronLeft,
@@ -11,6 +13,7 @@ import {
Minus,
Plus,
SlidersVertical,
X,
} from '@signozhq/icons';
import './LogsFormatOptionsMenu.styles.scss';
@@ -19,21 +22,14 @@ interface LogsFormatOptionsMenuProps {
items: any;
selectedOptionFormat: any;
config: OptionsMenuConfig;
onOpenColumns?: () => void;
}
interface OptionsMenuContentProps extends LogsFormatOptionsMenuProps {
closePopover: () => void;
}
function OptionsMenu({
items,
selectedOptionFormat,
config,
onOpenColumns,
closePopover,
}: OptionsMenuContentProps): JSX.Element {
const { maxLines, format, fontSize } = config;
}: LogsFormatOptionsMenuProps): JSX.Element {
const { maxLines, format, addColumn, fontSize } = config;
const [selectedItem, setSelectedItem] = useState(selectedOptionFormat);
const maxLinesNumber = (maxLines?.value as number) || 1;
const [maxLinesPerRow, setMaxLinesPerRow] = useState<number>(maxLinesNumber);
@@ -43,6 +39,13 @@ function OptionsMenu({
const [isFontSizeOptionsOpen, setIsFontSizeOptionsOpen] =
useState<boolean>(false);
const [showAddNewColumnContainer, setShowAddNewColumnContainer] =
useState(false);
const [selectedValue, setSelectedValue] = useState<string | null>(null);
const listRef = useRef<HTMLDivElement>(null);
const initialMouseEnterRef = useRef<boolean>(false);
const onChange = useCallback(
(key: LogViewMode) => {
if (!format) {
@@ -57,6 +60,7 @@ function OptionsMenu({
const handleMenuItemClick = (key: LogViewMode): void => {
setSelectedItem(key);
onChange(key);
setShowAddNewColumnContainer(false);
};
const incrementMaxLinesPerRow = (): void => {
@@ -71,6 +75,20 @@ function OptionsMenu({
}
};
const handleSearchValueChange = useDebouncedFn((event): void => {
// @ts-expect-error
const value = event?.target?.value || '';
if (addColumn && addColumn?.onSearch) {
addColumn?.onSearch(value);
}
}, 300);
const handleToggleAddNewColumn = (): void => {
addColumn?.onSearch?.('');
setShowAddNewColumnContainer(!showAddNewColumnContainer);
};
const handleLinesPerRowChange = (maxLinesPerRow: number | null): void => {
if (
maxLinesPerRow &&
@@ -81,11 +99,6 @@ function OptionsMenu({
}
};
const handleEditColumns = (): void => {
onOpenColumns?.();
closePopover();
};
useEffect(() => {
if (maxLinesPerRow && config && config.maxLines?.onChange) {
config.maxLines.onChange(maxLinesPerRow);
@@ -98,10 +111,110 @@ function OptionsMenu({
}
}, [fontSizeValue]);
function handleColumnSelection(
currentIndex: number,
optionsData: DefaultOptionType[],
): void {
const currentItem = optionsData[currentIndex];
const itemLength = optionsData.length;
if (addColumn && addColumn?.onSelect) {
addColumn?.onSelect(selectedValue, {
label: currentItem.label,
disabled: false,
});
// if the last element is selected then select the previous one
if (currentIndex === itemLength - 1) {
// there should be more than 1 element in the list
if (currentIndex - 1 >= 0) {
const prevValue = optionsData[currentIndex - 1]?.value || null;
setSelectedValue(prevValue as string | null);
} else {
// if there is only one element then just select and do nothing
setSelectedValue(null);
}
} else {
// selecting any random element from the list except the last one
const nextIndex = currentIndex + 1;
const nextValue = optionsData[nextIndex]?.value || null;
setSelectedValue(nextValue as string | null);
}
}
}
const handleKeyDown = (e: KeyboardEvent): void => {
if (!selectedValue) {
return;
}
const optionsData = addColumn?.options || [];
const currentIndex = optionsData.findIndex(
(item) => item?.value === selectedValue,
);
const itemLength = optionsData.length;
switch (e.key) {
case 'ArrowUp': {
const newValue = optionsData[Math.max(0, currentIndex - 1)]?.value;
setSelectedValue(newValue as string | null);
e.preventDefault();
break;
}
case 'ArrowDown': {
const newValue =
optionsData[Math.min(itemLength - 1, currentIndex + 1)]?.value;
setSelectedValue(newValue as string | null);
e.preventDefault();
break;
}
case 'Enter':
e.preventDefault();
handleColumnSelection(currentIndex, optionsData);
break;
default:
break;
}
};
useEffect(() => {
// Scroll the selected item into view
const listNode = listRef.current;
if (listNode && selectedValue) {
const optionsData = addColumn?.options || [];
const currentIndex = optionsData.findIndex(
(item) => item?.value === selectedValue,
);
const itemNode = listNode.children[currentIndex] as HTMLElement;
if (itemNode) {
itemNode.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
}
}
}, [selectedValue]);
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return (): void => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [selectedValue]);
return (
<div
className="nested-menu-container"
className={cx(
'nested-menu-container',
showAddNewColumnContainer ? 'active' : '',
)}
onClick={(event): void => {
// this is to restrict click events to propogate to parent
event.stopPropagation();
}}
>
@@ -155,7 +268,71 @@ function OptionsMenu({
</Button>
</div>
</div>
) : (
) : null}
{showAddNewColumnContainer && (
<div className="add-new-column-container">
<div className="add-new-column-header">
<div className="title">
<div className="periscope-btn ghost" onClick={handleToggleAddNewColumn}>
<ChevronLeft
size={14}
className="back-icon"
onClick={handleToggleAddNewColumn}
/>
</div>
Add New Column
</div>
<Input
tabIndex={0}
type="text"
autoFocus
onFocus={addColumn?.onFocus}
onChange={handleSearchValueChange}
placeholder="Search..."
/>
</div>
<div className="add-new-column-content">
{addColumn?.isFetching && (
<div className="loading-container"> Loading ... </div>
)}
<div className="column-format-new-options" ref={listRef}>
{addColumn?.options?.map(({ label, value }, index) => (
<div
className={cx('column-name', value === selectedValue && 'selected')}
key={value}
onMouseEnter={(): void => {
if (!initialMouseEnterRef.current) {
setSelectedValue(value as string | null);
}
initialMouseEnterRef.current = true;
}}
onMouseMove={(): void => {
// this is added to handle the mouse move explicit event and not the re-rendered on mouse enter event
setSelectedValue(value as string | null);
}}
onClick={(eve): void => {
eve.stopPropagation();
handleColumnSelection(index, addColumn?.options || []);
}}
>
<div className="name">
<Tooltip placement="left" title={label}>
{label}
</Tooltip>
</div>
</div>
))}
</div>
</div>
</div>
)}
{!isFontSizeOptionsOpen && !showAddNewColumnContainer && (
<div>
<div className="font-size-container">
<div className="title">Font Size</div>
@@ -195,52 +372,72 @@ function OptionsMenu({
{selectedItem && (
<>
<div className="horizontal-line" />
<div className="max-lines-per-row">
<div className="title"> max lines per row </div>
<div className="raw-format max-lines-per-row-input">
<button
type="button"
className="periscope-btn"
onClick={decrementMaxLinesPerRow}
>
{' '}
<Minus size={12} />{' '}
</button>
<InputNumber
min={1}
max={10}
value={maxLinesPerRow}
onChange={handleLinesPerRowChange}
/>
<button
type="button"
className="periscope-btn"
onClick={incrementMaxLinesPerRow}
>
{' '}
<Plus size={12} />{' '}
</button>
<>
<div className="horizontal-line" />
<div className="max-lines-per-row">
<div className="title"> max lines per row </div>
<div className="raw-format max-lines-per-row-input">
<button
type="button"
className="periscope-btn"
onClick={decrementMaxLinesPerRow}
>
{' '}
<Minus size={12} />{' '}
</button>
<InputNumber
min={1}
max={10}
value={maxLinesPerRow}
onChange={handleLinesPerRowChange}
/>
<button
type="button"
className="periscope-btn"
onClick={incrementMaxLinesPerRow}
>
{' '}
<Plus size={12} />{' '}
</button>
</div>
</div>
</div>
</>
)}
</>
{selectedItem === 'table' && onOpenColumns && (
<>
<div className="horizontal-line" />
<div className="edit-columns-container">
<Button
className="edit-columns-btn"
type="text"
onClick={handleEditColumns}
data-testid="periscope-btn-edit-columns"
>
<Typography.Text className="edit-columns-text">
Edit columns
</Typography.Text>
<ChevronRight size={14} className="icon" />
</Button>
<div className="selected-item-content-container active">
{!showAddNewColumnContainer && <div className="horizontal-line" />}
<div className="item-content">
{!showAddNewColumnContainer && (
<div className="title">
columns
<Plus size={14} onClick={handleToggleAddNewColumn} />{' '}
</div>
)}
<div className="column-format">
{addColumn?.value?.map(({ name }) => (
<div className="column-name" key={name}>
<div className="name">
<Tooltip placement="left" title={name}>
{name}
</Tooltip>
</div>
{addColumn?.value?.length > 1 && (
<X
className="delete-btn"
size={14}
onClick={(): void => addColumn.onRemove(name)}
/>
)}
</div>
))}
{addColumn && addColumn?.value?.length === 0 && (
<div className="column-name no-columns-selected">
No columns selected
</div>
)}
</div>
</div>
</div>
</>
)}
@@ -254,7 +451,6 @@ function LogsFormatOptionsMenu({
items,
selectedOptionFormat,
config,
onOpenColumns,
}: LogsFormatOptionsMenuProps): JSX.Element {
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
return (
@@ -264,8 +460,6 @@ function LogsFormatOptionsMenu({
items={items}
selectedOptionFormat={selectedOptionFormat}
config={config}
onOpenColumns={onOpenColumns}
closePopover={(): void => setIsPopoverOpen(false)}
/>
}
trigger="click"

View File

@@ -62,7 +62,6 @@ describe('LogsFormatOptionsMenu (unit)', () => {
onSearch: jest.fn(),
onSelect: jest.fn(),
onRemove: jest.fn(),
onReorder: jest.fn(),
},
}}
/>,
@@ -155,66 +154,4 @@ describe('LogsFormatOptionsMenu (unit)', () => {
expect(fontSizeOnChange).toHaveBeenCalledWith(FontSize.MEDIUM);
});
});
function renderWithOnOpen(
onOpenColumns?: jest.Mock,
selectedOptionFormat: 'table' | 'raw' | 'list' = 'table',
): { getByTestId: ReturnType<typeof render>['getByTestId'] } {
const items = [
{ key: 'raw', label: 'Raw', data: { title: 'max lines per row' } },
{ key: 'list', label: 'Default' },
{ key: 'table', label: 'Column', data: { title: 'columns' } },
];
const { getByTestId } = render(
<LogsFormatOptionsMenu
items={items}
selectedOptionFormat={selectedOptionFormat}
config={{
format: { value: selectedOptionFormat, onChange: jest.fn() },
maxLines: { value: 1, onChange: jest.fn() },
fontSize: { value: FontSize.SMALL, onChange: jest.fn() },
}}
onOpenColumns={onOpenColumns}
/>,
);
fireEvent.click(getByTestId('periscope-btn-format-options'));
return { getByTestId };
}
it('renders "Edit columns" row when format=table and onOpenColumns provided', () => {
const onOpenColumns = jest.fn();
const { getByTestId } = renderWithOnOpen(onOpenColumns, 'table');
expect(getByTestId('periscope-btn-edit-columns')).toBeInTheDocument();
});
it('does not render "Edit columns" row when onOpenColumns is not provided', () => {
renderWithOnOpen(undefined, 'table');
expect(
document.querySelector('[data-testid="periscope-btn-edit-columns"]'),
).toBeNull();
});
it('does not render "Edit columns" row when format is not table', () => {
renderWithOnOpen(jest.fn(), 'raw');
expect(
document.querySelector('[data-testid="periscope-btn-edit-columns"]'),
).toBeNull();
});
it('fires onOpenColumns and closes the popover when "Edit columns" is clicked', async () => {
const onOpenColumns = jest.fn();
const { getByTestId } = renderWithOnOpen(onOpenColumns, 'table');
fireEvent.click(getByTestId('periscope-btn-edit-columns'));
expect(onOpenColumns).toHaveBeenCalledTimes(1);
await waitFor(() => {
// Popover content unmounts on close.
expect(document.querySelector('.menu-container')).toBeNull();
});
});
});

View File

@@ -18,8 +18,7 @@ import {
RefreshCw,
} from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Select } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Button, Checkbox, Select } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import TextToolTip from 'components/TextToolTip/TextToolTip';
@@ -750,7 +749,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
tabIndex={isActive ? 0 : -1}
>
<Checkbox
value={isSelected}
checked={isSelected}
className="option-checkbox"
onClick={(e): void => {
e.stopPropagation();
@@ -1585,7 +1584,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
}}
>
<div style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Checkbox value={allOptionsSelected} className="option-checkbox">
<Checkbox checked={allOptionsSelected} className="option-checkbox">
<div className="option-content">
<div className="all-option-text">ALL</div>
</div>

View File

@@ -1,86 +0,0 @@
@keyframes noz-wave-wiggle {
0%,
100% {
transform: rotate(0deg);
}
25% {
transform: rotate(20deg);
}
75% {
transform: rotate(-20deg);
}
}
.noz {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 0;
overflow: visible;
svg {
display: block;
overflow: visible;
}
:global {
.noz-arm-left {
transform-origin: 4.18383px 13.4752px;
transform: rotate(0deg);
transition: transform 0.55s cubic-bezier(0.34, 1.7, 0.5, 1);
will-change: transform;
}
.noz-arm-wiggle {
transform-origin: 4.18383px 13.4752px;
transform: rotate(0deg);
will-change: transform;
}
.noz-head {
transform-origin: 12.02px 18.37px;
transform: rotate(0deg);
transition: transform 0.5s cubic-bezier(0.34, 1.7, 0.5, 1);
will-change: transform;
}
}
&:hover {
:global(.noz-arm-left) {
transform: rotate(145deg) scale(1, 1.7);
}
:global(.noz-arm-wiggle) {
animation: noz-wave-wiggle 0.7s ease-in-out 0.2s infinite;
}
:global(.noz-head) {
transform: rotate(9deg);
}
}
}
:global(.noz-wave):hover {
:global(.noz-arm-left) {
transform: rotate(145deg) scale(1, 1.7);
}
:global(.noz-arm-wiggle) {
animation: noz-wave-wiggle 0.7s ease-in-out 0.2s infinite;
}
:global(.noz-head) {
transform: rotate(9deg);
}
}
@media (prefers-reduced-motion: reduce) {
.noz {
:global(.noz-arm-left),
:global(.noz-arm-wiggle),
:global(.noz-head) {
transition: none;
animation: none !important;
}
}
}

View File

@@ -1,100 +0,0 @@
import cx from 'classnames';
import styles from './Noz.module.scss';
interface NozProps {
size?: number;
className?: string;
}
/**
* Noz — the SigNoz AI assistant mascot. Waves on hover.
*
* Hover behavior:
* - Hovering the icon itself triggers the wave.
* - To make a parent element (e.g. a Button) trigger the wave on its own
* hover, add the class `noz-wave` to that parent.
*/
export default function Noz({ size = 24, className }: NozProps): JSX.Element {
return (
<span
className={cx(styles.noz, className)}
style={{ width: size, height: size }}
aria-hidden="true"
>
<svg
viewBox="-2 0.5 28 28"
width={size}
height={size}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* body */}
<rect
x="4.35938"
y="8.49908"
width="15.4569"
height="11.978"
rx="1.76147"
fill="#E5484D"
/>
{/* legs */}
<rect
x="6.87012"
y="19.0679"
width="3.34679"
height="3.69908"
rx="0.880734"
fill="#E5484D"
/>
<rect
x="13.916"
y="19.0679"
width="3.34679"
height="3.69908"
rx="0.880734"
fill="#E5484D"
/>
{/* right arm (static) */}
<rect
x="18.7598"
y="13.4752"
width="2.11376"
height="3.69908"
rx="0.880734"
fill="#E5484D"
/>
{/* left arm: outer group does the "lift", inner does the wiggle */}
<g className="noz-arm-left">
<g className="noz-arm-wiggle">
<rect
x="3.12695"
y="13.4752"
width="2.11376"
height="3.69908"
rx="0.880734"
fill="#E5484D"
/>
</g>
</g>
{/* head: face + eye + hat tilt together */}
<g className="noz-head">
<circle cx="12.0217" cy="14.4881" r="3.87523" fill="#F5F5F5" />
<path
d="M12.0237 12.8024C12.0237 13.7328 11.2673 14.4892 10.337 14.4892C10.0339 14.4892 9.74926 14.4101 9.50152 14.2678C9.47517 14.5551 9.49888 14.8502 9.57795 15.1428C9.93901 16.4921 11.3279 17.2933 12.6773 16.9323C14.0267 16.5712 14.8279 15.1823 14.4668 13.8329C14.1453 12.6285 13.0041 11.8616 11.8023 11.967C11.942 12.2121 12.0237 12.4967 12.0237 12.8024Z"
fill="#0A0C10"
/>
<path
d="M8.33833 7.94578L9.83358 4.31319C10.1302 3.59261 10.6676 2.99939 11.355 2.63299L13.9181 1.26684C14.1327 1.15169 14.3804 1.34885 14.3194 1.58439L13.6703 4.06892C13.6511 4.14046 13.6424 4.21374 13.6424 4.28876C13.6424 4.39868 13.6633 4.5086 13.7052 4.61154L15.0382 7.94578H11.4248L11.6307 7.32813L12.3356 7.09259C12.449 7.05421 12.5257 6.94778 12.5257 6.82739C12.5257 6.707 12.449 6.60057 12.3356 6.56218L11.6307 6.32664L11.3951 5.62176C11.3568 5.51009 11.2503 5.43333 11.1299 5.43333C11.0096 5.43333 10.9031 5.5101 10.8647 5.6235L10.6292 6.32839L9.92431 6.56393C9.8109 6.60231 9.73413 6.70874 9.73413 6.82913C9.73413 6.94952 9.8109 7.05595 9.92431 7.09434L10.6292 7.32988L10.8351 7.94752H8.33833V7.94578ZM12.1 3.43558C12.0808 3.378 12.0285 3.33962 11.9674 3.33962C11.9064 3.33962 11.854 3.378 11.8348 3.43558L11.7179 3.78802L11.3655 3.90492C11.3079 3.92411 11.2695 3.97645 11.2695 4.03752C11.2695 4.09859 11.3079 4.15093 11.3655 4.17012L11.7179 4.28702L11.8348 4.63946C11.854 4.69704 11.9064 4.73542 11.9674 4.73542C12.0285 4.73542 12.0808 4.69704 12.1 4.63946L12.2169 4.28702L12.5694 4.17012C12.6269 4.15093 12.6653 4.09859 12.6653 4.03752C12.6653 3.97645 12.6269 3.92411 12.5694 3.90492L12.2169 3.78802L12.1 3.43558ZM7.78 7.91088H15.5965C15.9053 7.91088 16.1548 8.16038 16.1548 8.4692C16.1548 8.77803 15.9053 9.02753 15.5965 9.02753H7.78C7.47118 9.02753 7.22168 8.77803 7.22168 8.4692C7.22168 8.16038 7.47118 7.91088 7.78 7.91088Z"
fill="#4E74F8"
/>
</g>
</svg>
</span>
);
}
Noz.defaultProps = {
size: 24,
className: undefined,
};

View File

@@ -2,13 +2,6 @@
.query-add-ons {
width: 100%;
.add-on-tab-title {
display: flex;
align-items: center;
justify-content: center;
gap: var(--margin-2);
}
}
.add-ons-list {
@@ -32,7 +25,7 @@
color: var(--l2-foreground);
}
> button {
.tab {
border: 1px solid var(--l1-border);
border-left: none;
min-width: 120px;
@@ -42,21 +35,21 @@
&:first-child {
border-left: 1px solid var(--l1-border);
}
}
&::before {
background: var(--l1-border);
}
.tab::before {
background: var(--l1-border);
}
&[data-state='on'] {
color: var(--text-robin-500);
border: 1px solid var(--l1-border);
.selected-view {
color: var(--text-robin-500);
border: 1px solid var(--l1-border);
display: none;
display: none;
}
&::before {
background: var(--l1-border);
}
}
.selected-view::before {
background: var(--l1-border);
}
}
@@ -266,14 +259,6 @@
border-left: transparent;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
&:focus:not(:focus-visible),
&.ant-btn:focus:not(:focus-visible) {
border-color: var(--l2-border);
border-left-color: transparent;
outline: none;
box-shadow: none;
}
}
}
}
@@ -299,21 +284,5 @@
.cm-placeholder {
font-size: 12px !important;
}
$add-on-row-height: 38px;
.periscope-input-with-label {
.input {
.ant-select {
height: $add-on-row-height;
}
}
}
.input-with-label {
.input {
height: $add-on-row-height;
}
}
}
}

View File

@@ -1,6 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Tooltip } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Button, Radio, RadioChangeEvent, Tooltip } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter';
@@ -251,7 +250,8 @@ function QueryAddOns({
);
}, [panelType, isListViewPanel, query, showReduceTo]);
const handleOptionClick = (clickedAddOn: AddOn): void => {
const handleOptionClick = (e: RadioChangeEvent): void => {
const clickedAddOn = e.target.value as AddOn;
const isAlreadySelected = selectedViews.some(
(view) => view.key === clickedAddOn.key,
);
@@ -515,27 +515,15 @@ function QueryAddOns({
</div>
)}
<ToggleGroupSimple
type="multiple"
className="add-ons-tabs"
value={selectedViews.map((view) => view.key)}
onChange={(newKeys: string[]): void => {
const oldKeys = selectedViews.map((view) => view.key);
const toggledKey =
newKeys.find((k) => !oldKeys.includes(k)) ??
oldKeys.find((k) => !newKeys.includes(k));
if (!toggledKey) {
return;
}
const clickedAddOn = addOns.find((a) => a.key === toggledKey);
if (clickedAddOn) {
handleOptionClick(clickedAddOn);
}
}}
items={addOns.map((addOn) => ({
value: addOn.key,
label: (
<div className="add-ons-list">
<Radio.Group
className="add-ons-tabs"
onChange={handleOptionClick}
value={selectedViews}
>
{addOns.map((addOn) => (
<Tooltip
key={addOn.key}
title={
<TooltipContent
label={addOn.label}
@@ -546,17 +534,26 @@ function QueryAddOns({
placement="top"
mouseEnterDelay={0.5}
>
<span
className="add-on-tab-title"
data-testid={`query-add-on-${addOn.key}`}
<Radio.Button
className={
selectedViews.find((view) => view.key === addOn.key)
? 'selected-view tab'
: 'tab'
}
value={addOn}
>
{addOn.icon}
{addOn.label}
</span>
<div
className="add-on-tab-title"
data-testid={`query-add-on-${addOn.key}`}
>
{addOn.icon}
{addOn.label}
</div>
</Radio.Button>
</Tooltip>
),
}))}
/>
))}
</Radio.Group>
</div>
</div>
);
}

View File

@@ -14,8 +14,7 @@ import { Color } from '@signozhq/design-tokens';
import { copilot } from '@uiw/codemirror-theme-copilot';
import { githubLight } from '@uiw/codemirror-theme-github';
import CodeMirror, { EditorView, keymap, Prec } from '@uiw/react-codemirror';
import { Button, Card, Collapse, Popover, Tooltip } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { Button, Card, Collapse, Popover, Tag, Tooltip } from 'antd';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
import cx from 'classnames';
@@ -665,26 +664,26 @@ function QuerySearch({
// Helper function to render a badge for the current context mode
const renderContextBadge = (): JSX.Element => {
if (!editingMode) {
return <Badge color="vanilla">Unknown</Badge>;
return <Tag>Unknown</Tag>;
}
switch (editingMode) {
case 'key':
return <Badge color="robin">Key</Badge>;
return <Tag color="blue">Key</Tag>;
case 'operator':
return <Badge color="sakura">Operator</Badge>;
return <Tag color="purple">Operator</Tag>;
case 'value':
return <Badge color="forest">Value</Badge>;
return <Tag color="green">Value</Tag>;
case 'conjunction':
return <Badge color="amber">Conjunction</Badge>;
return <Tag color="orange">Conjunction</Tag>;
case 'function':
return <Badge color="aqua">Function</Badge>;
return <Tag color="cyan">Function</Tag>;
case 'parenthesis':
return <Badge color="sakura">Parenthesis</Badge>;
return <Tag color="magenta">Parenthesis</Tag>;
case 'bracketList':
return <Badge color="cherry">Bracket List</Badge>;
return <Tag color="red">Bracket List</Tag>;
default:
return <Badge color="vanilla">Unknown</Badge>;
return <Tag>Unknown</Tag>;
}
};
@@ -1305,37 +1304,34 @@ function QuerySearch({
Currently editing: {renderContextBadge()}
{queryContext?.keyToken && (
<span className="triplet-info">
Key: <Badge color="vanilla">{queryContext.keyToken}</Badge>
Key: <Tag>{queryContext.keyToken}</Tag>
</span>
)}
{queryContext?.operatorToken && (
<span className="triplet-info">
Operator: <Badge color="vanilla">{queryContext.operatorToken}</Badge>
Operator: <Tag>{queryContext.operatorToken}</Tag>
</span>
)}
{queryContext?.valueToken && (
<span className="triplet-info">
Value: <Badge color="vanilla">{queryContext.valueToken}</Badge>
Value: <Tag>{queryContext.valueToken}</Tag>
</span>
)}
{queryContext?.currentPair && (
<span className="triplet-info query-pair-info">
Current pair: <Badge color="robin">{queryContext.currentPair.key}</Badge>
<Badge color="sakura">{queryContext.currentPair.operator}</Badge>
Current pair: <Tag color="blue">{queryContext.currentPair.key}</Tag>
<Tag color="purple">{queryContext.currentPair.operator}</Tag>
{queryContext.currentPair.value && (
<Badge color="forest">{queryContext.currentPair.value}</Badge>
<Tag color="green">{queryContext.currentPair.value}</Tag>
)}
<Badge
color={queryContext.currentPair.isComplete ? 'success' : 'warning'}
>
<Tag color={queryContext.currentPair.isComplete ? 'success' : 'warning'}>
{queryContext.currentPair.isComplete ? 'Complete' : 'Incomplete'}
</Badge>
</Tag>
</span>
)}
{queryContext?.queryPairs && queryContext.queryPairs.length > 0 && (
<span className="triplet-info">
Total pairs:{' '}
<Badge color="robin">{queryContext.queryPairs.length}</Badge>
Total pairs: <Tag color="blue">{queryContext.queryPairs.length}</Tag>
</span>
)}
</div>

View File

@@ -6,12 +6,12 @@ import {
useMemo,
useState,
} from 'react';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import { Dropdown } from 'antd';
import cx from 'classnames';
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import QBEntityOptions from 'container/QueryBuilder/components/QBEntityOptions/QBEntityOptions';
import { QueryProps } from 'container/QueryBuilder/type';
import { QueryProps } from 'container/QueryBuilder/components/Query/Query.interfaces';
import SpanScopeSelector from 'container/QueryBuilder/filters/QueryBuilderSearchV2/SpanScopeSelector';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
@@ -195,7 +195,7 @@ export const QueryV2 = forwardRef(function QueryV2(
)}
{isMultiQueryAllowed && (
<DropdownMenuSimple
<Dropdown
className="query-actions-dropdown"
menu={{
items: [
@@ -217,10 +217,10 @@ export const QueryV2 = forwardRef(function QueryV2(
: []),
],
}}
align="end"
placement="bottomRight"
>
<Ellipsis size={16} />
</DropdownMenuSimple>
</Dropdown>
)}
</div>
</div>

View File

@@ -4,23 +4,6 @@
padding: 12px;
gap: 12px;
border-bottom: 1px solid var(--l1-border);
.search {
input {
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
--input-focus-background: var(--l2-background);
&::placeholder {
color: var(--l3-foreground);
}
--input-font-size: 14px;
--input-border-color: var(--l1-border);
--input-focus-border-color: var(--primary-background);
--input-focus-outline-width: 0;
--input-focus-outline-offset: 0;
}
}
.filter-header-checkbox {
display: flex;
align-items: center;
@@ -72,7 +55,6 @@
display: flex;
align-items: center;
gap: 8px;
min-height: 24px;
.checkbox-value-section {
display: flex;

View File

@@ -1,8 +1,6 @@
/* eslint-disable sonarjs/no-identical-functions */
import { Fragment, useMemo, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Button, Skeleton } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Button, Checkbox, Input, Skeleton } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
@@ -636,12 +634,10 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
)}
<div className="value">
<Checkbox
onChange={(checked): void =>
onChange(value, checked === true, false)
}
value={currentFilterState[value]}
onChange={(e): void => onChange(value, e.target.checked, false)}
checked={currentFilterState[value]}
disabled={isFilterDisabled}
className="check-box"
rootClassName="check-box"
/>
<div

View File

@@ -1,6 +1,5 @@
import { useMemo } from 'react';
import { Input } from '@signozhq/ui/input';
import { Button } from 'antd';
import { Button, Input } from 'antd';
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';

View File

@@ -4,14 +4,14 @@ import type {
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
} from 'antd';
import { Button, Flex } from 'antd';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import { Button, Dropdown, Flex, MenuProps } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { SlidersHorizontal } from '@signozhq/icons';
import { popupContainer } from 'utils/selectPopupContainer';
import ResizeTable from './ResizeTable';
import { DynamicColumnTableProps } from './types';
@@ -84,9 +84,8 @@ function DynamicColumnTable({
);
};
const items: MenuItem[] =
const items: MenuProps['items'] =
dynamicColumns?.map((column, index) => ({
key: String(index),
label: (
<div
className="dynamicColumnsTable-items"
@@ -100,6 +99,8 @@ function DynamicColumnTable({
/>
</div>
),
key: index,
type: 'checkbox',
})) || [];
// Get current page from URL or default to 1
@@ -128,14 +129,18 @@ function DynamicColumnTable({
<Flex justify="flex-end" align="center" gap={8}>
{facingIssueBtn && <LaunchChatSupport {...facingIssueBtn} />}
{dynamicColumns && (
<DropdownMenuSimple menu={{ items }}>
<Dropdown
getPopupContainer={popupContainer}
menu={{ items }}
trigger={['click']}
>
<Button
className="dynamicColumnTable-button filter-btn"
size="middle"
icon={<SlidersHorizontal size={14} />}
data-testid="additional-filters-button"
/>
</DropdownMenuSimple>
</Dropdown>
)}
</Flex>

View File

@@ -1,6 +1,5 @@
import { CircleAlert, RefreshCw } from '@signozhq/icons';
import { Select } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Checkbox, Select } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useListRoles } from 'api/generated/services/role';
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
@@ -147,11 +146,12 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
options={options}
optionFilterProp="label"
optionRender={(option): JSX.Element => (
<div style={{ pointerEvents: 'none' }}>
<Checkbox value={value.includes(option.value as string)}>
{option.label}
</Checkbox>
</div>
<Checkbox
checked={value.includes(option.value as string)}
style={{ pointerEvents: 'none' }}
>
{option.label}
</Checkbox>
)}
getPopupContainer={getPopupContainer}
disabled={disabled}

View File

@@ -2,7 +2,7 @@ import type { Control, UseFormRegister } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import { DatePicker } from 'antd';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
import {
@@ -60,21 +60,30 @@ function KeyFormPhase({
name="expiryMode"
control={control}
render={({ field }): JSX.Element => (
<ToggleGroupSimple
<ToggleGroup
type="single"
value={field.value}
onChange={(val: string): void => {
onChange={(val): void => {
if (val) {
field.onChange(val);
}
}}
size="sm"
className="add-key-modal__expiry-toggle"
items={[
{ value: ExpiryMode.NONE, label: 'No Expiration' },
{ value: ExpiryMode.DATE, label: 'Set Expiration Date' },
]}
/>
>
<ToggleGroupItem
value={ExpiryMode.NONE}
className="add-key-modal__expiry-toggle-btn"
>
No Expiration
</ToggleGroupItem>
<ToggleGroupItem
value={ExpiryMode.DATE}
className="add-key-modal__expiry-toggle-btn"
>
Set Expiration Date
</ToggleGroupItem>
</ToggleGroup>
)}
/>
</div>

View File

@@ -4,7 +4,7 @@ import { LockKeyhole, Trash2, X } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import { DatePicker } from 'antd';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
@@ -101,22 +101,31 @@ function EditKeyForm({
name="expiryMode"
control={control}
render={({ field }): JSX.Element => (
<ToggleGroupSimple
<ToggleGroup
type="single"
value={field.value}
onChange={(val: string): void => {
onChange={(val): void => {
if (val && canUpdate) {
field.onChange(val);
}
}}
size="sm"
disabled={!canUpdate}
className="edit-key-modal__expiry-toggle"
items={[
{ value: ExpiryMode.NONE, label: 'No Expiration' },
{ value: ExpiryMode.DATE, label: 'Set Expiration Date' },
]}
/>
>
<ToggleGroupItem
value={ExpiryMode.NONE}
disabled={!canUpdate}
className="edit-key-modal__expiry-toggle-btn"
>
No Expiration
</ToggleGroupItem>
<ToggleGroupItem
value={ExpiryMode.DATE}
disabled={!canUpdate}
className="edit-key-modal__expiry-toggle-btn"
>
Set Expiration Date
</ToggleGroupItem>
</ToggleGroup>
)}
/>
</div>

View File

@@ -4,7 +4,7 @@ import { Key, LayoutGrid, Plus, Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DrawerWrapper } from '@signozhq/ui/drawer';
import { toast } from '@signozhq/ui/sonner';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import { Pagination, Skeleton } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
@@ -395,11 +395,11 @@ function ServiceAccountDrawer({
const drawerContent = (
<div className="sa-drawer__layout">
<div className="sa-drawer__tabs">
<ToggleGroupSimple
<ToggleGroup
type="single"
value={activeTab}
size="sm"
onChange={(val: string): void => {
onChange={(val): void => {
if (val) {
void setActiveTab(val as ServiceAccountDrawerTab);
if (val !== ServiceAccountDrawerTab.Keys) {
@@ -409,30 +409,25 @@ function ServiceAccountDrawer({
}
}}
className="sa-drawer__tab-group"
items={[
{
value: ServiceAccountDrawerTab.Overview,
label: (
<>
<LayoutGrid size={14} />
Overview
</>
),
},
{
value: ServiceAccountDrawerTab.Keys,
label: (
<>
<Key size={14} />
Keys
{keys.length > 0 && (
<span className="sa-drawer__tab-count">{keys.length}</span>
)}
</>
),
},
]}
/>
>
<ToggleGroupItem
value={ServiceAccountDrawerTab.Overview}
className="sa-drawer__tab"
>
<LayoutGrid size={14} />
Overview
</ToggleGroupItem>
<ToggleGroupItem
value={ServiceAccountDrawerTab.Keys}
className="sa-drawer__tab"
>
<Key size={14} />
Keys
{keys.length > 0 && (
<span className="sa-drawer__tab-count">{keys.length}</span>
)}
</ToggleGroupItem>
</ToggleGroup>
{activeTab === ServiceAccountDrawerTab.Keys && (
<AuthZTooltip
checks={[

View File

@@ -1,6 +1,12 @@
.signoz-radio-group {
.signoz-radio-group.ant-radio-group {
color: var(--l2-foreground);
&.ant-radio-group-disabled {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
.view-title {
display: flex;
gap: var(--margin-2);
@@ -24,41 +30,43 @@
}
}
> button {
.tab {
border: 1px solid var(--l1-border);
background: var(--l1-background);
&:hover {
color: var(--l1-foreground);
}
&::before {
background: var(--l1-border);
}
}
&[data-state='on'] {
&,
&:hover {
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--l1-border);
}
&::before {
background: var(--l3-background);
border-left: 1px solid var(--l1-border);
}
.selected_view {
&,
&:hover {
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--l1-border);
}
&::before {
background: var(--l3-background);
border-left: 1px solid var(--l1-border);
}
}
&[disabled],
&[data-disabled] {
&.ant-radio-group-disabled {
.tab,
.selected_view {
background: var(--l2-background) !important;
border-color: var(--l1-border) !important;
color: var(--l1-foreground) !important;
}
&:hover {
background: var(--l2-background) !important;
border-color: var(--l1-border) !important;
color: var(--l1-foreground) !important;
}
.tab:hover,
.selected_view:hover {
background: var(--l2-background) !important;
border-color: var(--l1-border) !important;
color: var(--l1-foreground) !important;
}
}
}

View File

@@ -1,4 +1,4 @@
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Radio, RadioChangeEvent } from 'antd';
import './SignozRadioGroup.styles.scss';
@@ -11,7 +11,7 @@ interface Option {
interface SignozRadioGroupProps {
value: string;
options: Option[];
onChange: (value: string) => void;
onChange: (e: RadioChangeEvent) => void;
className?: string;
disabled?: boolean;
}
@@ -24,22 +24,26 @@ function SignozRadioGroup({
disabled = false,
}: SignozRadioGroupProps): JSX.Element {
return (
<ToggleGroupSimple
type="single"
<Radio.Group
value={value}
buttonStyle="solid"
className={`signoz-radio-group ${className}`}
onChange={onChange}
disabled={disabled}
items={options.map((option) => ({
value: option.value,
label: (
>
{options.map((option) => (
<Radio.Button
key={option.value}
value={option.value}
className={value === option.value ? 'selected_view tab' : 'tab'}
>
<div className="view-title-container">
{option.icon && <div className="icon-container">{option.icon}</div>}
{option.label}
</div>
),
}))}
/>
</Radio.Button>
))}
</Radio.Group>
);
}

View File

@@ -4,10 +4,13 @@ import {
ButtonProps,
Col,
ColProps,
Divider,
DividerProps,
Row,
RowProps,
Space,
SpaceProps,
TabsProps,
} from 'antd';
import {
Typography,
@@ -31,11 +34,21 @@ const StyledRow = styled(Row)<TStyledRow>`
${styledClass}
`;
type TStyledDivider = DividerProps & IStyledClass;
const StyledDivider = styled(Divider)<TStyledDivider>`
${styledClass}
`;
type TStyledSpace = SpaceProps & IStyledClass;
const StyledSpace = styled(Space)<TStyledSpace>`
${styledClass}
`;
type TStyledTabs = TabsProps & IStyledClass;
const StyledTabs = styled(Divider)<TStyledTabs>`
${styledClass}
`;
type TStyledButton = ButtonProps & IStyledClass;
const StyledButton = styled(Button)<TStyledButton>`
${styledClass}
@@ -66,7 +79,9 @@ export {
StyledButton,
StyledCol,
StyledDiv,
StyledDivider,
StyledRow,
StyledSpace,
StyledTabs,
StyledTypography,
};

View File

@@ -1,13 +1,12 @@
import { Popover } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { Popover, Tag } from 'antd';
import { LabelColumnProps } from './TableRenderer.types';
import BadgeWithTooltip from './BadgeWithTooltip';
import TagWithToolTip from './TagWithToolTip';
import { getLabelAndValueContent } from './utils';
import './LabelColumn.styles.scss';
function LabelColumn({ labels, value }: LabelColumnProps): JSX.Element {
function LabelColumn({ labels, value, color }: LabelColumnProps): JSX.Element {
const newLabels = labels.length > 3 ? labels.slice(0, 3) : labels;
const remainingLabels = labels.length > 3 ? labels.slice(3) : [];
@@ -15,7 +14,7 @@ function LabelColumn({ labels, value }: LabelColumnProps): JSX.Element {
<div className="label-column">
{newLabels.map(
(label: string): JSX.Element => (
<BadgeWithTooltip key={label} label={label} value={value} />
<TagWithToolTip key={label} label={label} color={color} value={value} />
),
)}
{remainingLabels.length > 0 && (
@@ -27,9 +26,9 @@ function LabelColumn({ labels, value }: LabelColumnProps): JSX.Element {
{labels.map(
(label: string): JSX.Element => (
<div key={label}>
<Badge className="label-column--tag" color="vanilla">
<Tag className="label-column--tag" color={color}>
{getLabelAndValueContent(label, value && value[label])}
</Badge>
</Tag>
</div>
),
)}
@@ -37,9 +36,9 @@ function LabelColumn({ labels, value }: LabelColumnProps): JSX.Element {
}
trigger="hover"
>
<Badge className="label-column--tag" color="vanilla">
<Tag className="label-column--tag" color={color}>
+{remainingLabels.length}
</Badge>
</Tag>
</Popover>
)}
</div>

View File

@@ -1,34 +1,36 @@
import { Tooltip } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { Tag, Tooltip } from 'antd';
import { getLabelRenderingValue } from './utils';
function BadgeWithTooltip({
function TagWithToolTip({
label,
value,
}: BadgeWithTooltipProps): JSX.Element {
color,
}: TagWithToolTipProps): JSX.Element {
const tooltipTitle =
value && value[label] ? `${label}: ${value[label]}` : label;
return (
<div key={label}>
<Tooltip title={tooltipTitle}>
<Badge className="label-column--tag" color="vanilla">
<Tag className="label-column--tag" color={color}>
{getLabelRenderingValue(label, value && value[label])}
</Badge>
</Tag>
</Tooltip>
</div>
);
}
type BadgeWithTooltipProps = {
type TagWithToolTipProps = {
label: string;
color?: string;
value?: {
[key: string]: string;
};
};
BadgeWithTooltip.defaultProps = {
TagWithToolTip.defaultProps = {
value: undefined,
color: undefined,
};
export default BadgeWithTooltip;
export default TagWithToolTip;

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