Compare commits

..

11 Commits

Author SHA1 Message Date
Nikhil Soni
ed87794dee chore: move flamegraph after aggregation to reduce diff 2026-06-02 16:46:25 +05:30
Nikhil Soni
055a6cbd9b chore: update openapi specs 2026-06-02 16:36:12 +05:30
Nikhil Soni
099510a497 chore: extract out flamegraph building logic for easier review 2026-06-02 16:30:33 +05:30
Nikhil Soni
f3e6534d35 chore: ignore nil assigment lint error 2026-06-02 16:30:33 +05:30
Nikhil Soni
09fe9fa7cb chore: move exported methods to the top 2026-06-02 16:30:31 +05:30
Nikhil Soni
8b64247efa feat: query full spans for smaller traces 2026-06-02 16:23:42 +05:30
Nikhil Soni
7948b3091f feat: add api and module for flamegraph v3 2026-06-02 16:21:10 +05:30
Nikhil Soni
9bef79f664 feat: add method to enrich selected spans 2026-06-02 16:11:16 +05:30
Nikhil Soni
aea4a011f9 feat: add config for flamegraph 2026-06-02 16:11:16 +05:30
Nikhil Soni
b07335d969 chore: remove limit from request payload
It's a new api so doesn't need to be backward compatible
2026-06-02 16:11:16 +05:30
Nikhil Soni
c5f002110a feat: add types for flamegraph v3 in module structure 2026-06-02 16:11:16 +05:30
543 changed files with 8906 additions and 23191 deletions

23
.github/CODEOWNERS vendored
View File

@@ -169,26 +169,3 @@ go.mod @therealpandey
## Dashboard V2
/frontend/src/pages/DashboardPageV2/ @SigNoz/pulse-frontend
/frontend/src/pages/DashboardsListPageV2/ @SigNoz/pulse-frontend
## Infrastructure Monitoring
/frontend/src/pages/InfrastructureMonitoring/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringHosts/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringK8s/ @SigNoz/pulse-frontend
## Alerts
/frontend/src/pages/AlertList/ @SigNoz/pulse-frontend
/frontend/src/pages/AlertDetails/ @SigNoz/pulse-frontend
/frontend/src/pages/CreateAlert/ @SigNoz/pulse-frontend
/frontend/src/pages/EditRules/ @SigNoz/pulse-frontend
/frontend/src/container/AlertHistory/ @SigNoz/pulse-frontend
/frontend/src/container/CreateAlertRule/ @SigNoz/pulse-frontend
/frontend/src/container/CreateAlertV2/ @SigNoz/pulse-frontend
/frontend/src/container/EditAlertV2/ @SigNoz/pulse-frontend
/frontend/src/container/FormAlertRules/ @SigNoz/pulse-frontend
/frontend/src/container/ListAlertRules/ @SigNoz/pulse-frontend
/frontend/src/container/TriggeredAlerts/ @SigNoz/pulse-frontend
/frontend/src/container/AnomalyAlertEvaluationView/ @SigNoz/pulse-frontend
## OpenAPI Schema - Generated
/frontend/src/api/generated/services/ @therealpandey @vikrantgupta25 @srikanthccv
/docs/api/openapi.yml @therealpandey @vikrantgupta25 @srikanthccv

View File

@@ -69,8 +69,6 @@ jobs:
echo 'VITE_APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> frontend/.env
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'VITE_DOCS_BASE_URL="https://signoz.io"' >> frontend/.env
echo 'VITE_ENVIRONMENT="production"' >> frontend/.env
echo 'VITE_VERSION="${{ steps.build-info.outputs.version }}"' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:

View File

@@ -70,8 +70,6 @@ jobs:
echo 'VITE_APPCUES_APP_ID="${{ secrets.NP_APPCUES_APP_ID }}"' >> frontend/.env
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.NP_PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'VITE_DOCS_BASE_URL="https://staging.signoz.io"' >> frontend/.env
echo 'VITE_ENVIRONMENT="staging"' >> frontend/.env
echo 'VITE_VERSION="${{ steps.build-info.outputs.version }}"' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:

View File

@@ -35,8 +35,6 @@ jobs:
echo 'VITE_APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> .env
echo 'VITE_DOCS_BASE_URL="https://signoz.io"' >> .env
echo 'VITE_ENVIRONMENT="production"' >> .env
echo 'VITE_VERSION="${{ github.ref_name }}"' >> .env
- name: node-setup
uses: actions/setup-node@v5
with:

View File

@@ -39,7 +39,6 @@ jobs:
matrix:
suite:
- alerts
- basepath
- callbackauthn
- cloudintegrations
- dashboard
@@ -53,7 +52,6 @@ jobs:
- rootuser
- serviceaccount
- querier_json_body
- querier_skip_resource_fingerprint
- ttl
sqlstore-provider:
- postgres
@@ -84,7 +82,7 @@ jobs:
run: |
cd tests && uv sync
- name: webdriver
if: matrix.suite == 'callbackauthn' || matrix.suite == 'basepath'
if: matrix.suite == 'callbackauthn'
run: |
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee -a /etc/apt/sources.list.d/google-chrome.list

View File

@@ -91,7 +91,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
sqlstoreProviderFactories(),
signoz.NewTelemetryStoreProviderFactories(),
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
return signoz.NewAuthNs(ctx, providerSettings, store, licensing, config.Global)
return signoz.NewAuthNs(ctx, providerSettings, store, licensing)
},
func(ctx context.Context, sqlstore sqlstore.SQLStore, config authz.Config, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore, config)

View File

@@ -107,17 +107,17 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
sqlstoreProviderFactories(),
signoz.NewTelemetryStoreProviderFactories(),
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
samlCallbackAuthN, err := samlcallbackauthn.New(ctx, store, licensing, config.Global)
samlCallbackAuthN, err := samlcallbackauthn.New(ctx, store, licensing)
if err != nil {
return nil, err
}
oidcCallbackAuthN, err := oidccallbackauthn.New(store, licensing, providerSettings, config.Global)
oidcCallbackAuthN, err := oidccallbackauthn.New(store, licensing, providerSettings)
if err != nil {
return nil, err
}
authNs, err := signoz.NewAuthNs(ctx, providerSettings, store, licensing, config.Global)
authNs, err := signoz.NewAuthNs(ctx, providerSettings, store, licensing)
if err != nil {
return nil, err
}

View File

@@ -432,7 +432,7 @@ cloudintegration:
version: v0.0.8
##################### Trace Detail #####################
traces:
tracedetail:
waterfall:
# Number of spans returned per request when the trace is too large to show all at once.
span_page_size: 500
@@ -440,6 +440,17 @@ traces:
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:

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.127.1
image: signoz/signoz:v0.126.1
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port
@@ -213,7 +213,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.5
image: signoz/signoz-otel-collector:v0.144.4
entrypoint:
- /bin/sh
command:
@@ -241,7 +241,7 @@ services:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.5
image: signoz/signoz-otel-collector:v0.144.4
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster

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.127.1
image: signoz/signoz:v0.126.1
ports:
- "8080:8080" # signoz port
volumes:
@@ -139,7 +139,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.5
image: signoz/signoz-otel-collector:v0.144.4
entrypoint:
- /bin/sh
command:
@@ -167,7 +167,7 @@ services:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.5
image: signoz/signoz-otel-collector:v0.144.4
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster

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.127.1}
image: signoz/signoz:${VERSION:-v0.126.1}
container_name: signoz
ports:
- "8080:8080" # signoz port
@@ -204,7 +204,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.5}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.4}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
@@ -229,7 +229,7 @@ services:
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.5}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.4}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

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.127.1}
image: signoz/signoz:${VERSION:-v0.126.1}
container_name: signoz
ports:
- "8080:8080" # signoz port
@@ -132,7 +132,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.5}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.4}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
@@ -157,7 +157,7 @@ services:
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.5}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.4}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

File diff suppressed because it is too large Load Diff

View File

@@ -5,12 +5,10 @@ import (
"fmt"
"log/slog"
"net/url"
"path"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/client"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/authtypes"
@@ -28,14 +26,13 @@ var defaultScopes []string = []string{"email", "profile", oidc.ScopeOpenID}
var _ authn.CallbackAuthN = (*AuthN)(nil)
type AuthN struct {
settings factory.ScopedProviderSettings
store authtypes.AuthNStore
licensing licensing.Licensing
httpClient *client.Client
globalConfig global.Config
settings factory.ScopedProviderSettings
store authtypes.AuthNStore
licensing licensing.Licensing
httpClient *client.Client
}
func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSettings factory.ProviderSettings, globalConfig global.Config) (*AuthN, error) {
func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSettings factory.ProviderSettings) (*AuthN, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn")
httpClient, err := client.New(providerSettings.Logger, providerSettings.TracerProvider, providerSettings.MeterProvider)
@@ -44,11 +41,10 @@ func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSett
}
return &AuthN{
settings: settings,
store: store,
licensing: licensing,
httpClient: httpClient,
globalConfig: globalConfig,
settings: settings,
store: store,
licensing: licensing,
httpClient: httpClient,
}, nil
}
@@ -201,7 +197,7 @@ func (a *AuthN) oidcProviderAndoauth2Config(ctx context.Context, siteURL *url.UR
RedirectURL: (&url.URL{
Scheme: siteURL.Scheme,
Host: siteURL.Host,
Path: path.Join(a.globalConfig.ExternalPath(), redirectPath),
Path: redirectPath,
}).String(),
}, nil
}

View File

@@ -6,12 +6,10 @@ import (
"encoding/base64"
"encoding/pem"
"net/url"
"path"
"strings"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -26,16 +24,14 @@ const (
var _ authn.CallbackAuthN = (*AuthN)(nil)
type AuthN struct {
store authtypes.AuthNStore
licensing licensing.Licensing
globalConfig global.Config
store authtypes.AuthNStore
licensing licensing.Licensing
}
func New(ctx context.Context, store authtypes.AuthNStore, licensing licensing.Licensing, globalConfig global.Config) (*AuthN, error) {
func New(ctx context.Context, store authtypes.AuthNStore, licensing licensing.Licensing) (*AuthN, error) {
return &AuthN{
store: store,
licensing: licensing,
globalConfig: globalConfig,
store: store,
licensing: licensing,
}, nil
}
@@ -136,7 +132,7 @@ func (a *AuthN) serviceProvider(siteURL *url.URL, authDomain *authtypes.AuthDoma
return nil, err
}
acsURL := &url.URL{Scheme: siteURL.Scheme, Host: siteURL.Host, Path: path.Join(a.globalConfig.ExternalPath(), redirectPath)}
acsURL := &url.URL{Scheme: siteURL.Scheme, Host: siteURL.Host, Path: redirectPath}
// Note:
// The ServiceProviderIssuer is the client id in case of keycloak. Since we set it to the host here, we need to set the client id == host in keycloak.

View File

@@ -355,32 +355,26 @@ func (module *module) GetService(ctx context.Context, orgID valuer.UUID, service
var integrationService *cloudintegrationtypes.CloudIntegrationService
if cloudIntegrationID.IsZero() {
return cloudintegrationtypes.NewService(provider, serviceDefinition, nil, nil), nil
if !cloudIntegrationID.IsZero() {
storedService, err := module.store.GetServiceByServiceID(ctx, cloudIntegrationID, serviceID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
if storedService != nil {
serviceConfig, err := cloudintegrationtypes.NewServiceConfigFromJSON(provider, storedService.Config)
if err != nil {
return nil, err
}
integrationService = cloudintegrationtypes.NewCloudIntegrationServiceFromStorable(storedService, serviceConfig)
}
if err := module.enrichDashboardIDs(ctx, orgID, provider, serviceID, serviceDefinition); err != nil {
return nil, err
}
}
storedService, err := module.store.GetServiceByServiceID(ctx, cloudIntegrationID, serviceID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
if storedService == nil {
return cloudintegrationtypes.NewService(provider, serviceDefinition, nil, nil), nil
}
serviceConfig, err := cloudintegrationtypes.NewServiceConfigFromJSON(provider, storedService.Config)
if err != nil {
return nil, err
}
integrationService = cloudintegrationtypes.NewCloudIntegrationServiceFromStorable(storedService, serviceConfig)
slugPrefix := cloudintegrationtypes.CloudIntegrationDashboardSlugPrefix(provider, serviceID)
integrationDashboards, err := module.store.ListIntegrationDashboardsBySlugPrefix(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slugPrefix)
if err != nil {
return nil, err
}
return cloudintegrationtypes.NewService(provider, serviceDefinition, integrationService, integrationDashboards), nil
return cloudintegrationtypes.NewService(*serviceDefinition, integrationService), nil
}
func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
@@ -589,3 +583,20 @@ func (module *module) deprovisionDashboards(ctx context.Context, orgID valuer.UU
}
return nil
}
// enrichDashboardIDs replaces the raw dashboard name in each Dashboard.ID with the provisioned UUID.
// TODO: remove this hack and send idiomatic response to client.
func (module *module) enrichDashboardIDs(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, serviceID cloudintegrationtypes.ServiceID, serviceDefinition *cloudintegrationtypes.ServiceDefinition) error {
for i, d := range serviceDefinition.Assets.Dashboards {
slug := cloudintegrationtypes.CloudIntegrationDashboardSlug(provider, serviceID, d.ID)
row, err := module.store.GetIntegrationDashboardBySlug(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
continue
}
return err
}
serviceDefinition.Assets.Dashboards[i].ID = row.DashboardID
}
return nil
}

View File

@@ -221,18 +221,6 @@ func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UU
return module.pkgDashboardModule.GetV2(ctx, orgID, id)
}
func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatable dashboardtypes.UpdatableDashboardV2) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.UpdateV2(ctx, orgID, id, updatedBy, updatable)
}
func (module *module) PatchV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, patch dashboardtypes.PatchableDashboardV2) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.PatchV2(ctx, orgID, id, updatedBy, patch)
}
func (module *module) LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
return module.pkgDashboardModule.LockUnlockV2(ctx, orgID, id, updatedBy, isAdmin, lock)
}
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

@@ -0,0 +1,28 @@
// Mock for useSafeNavigate hook to avoid React Router version conflicts in tests
interface SafeNavigateOptions {
replace?: boolean;
state?: unknown;
newTab?: boolean;
}
interface SafeNavigateTo {
pathname?: string;
search?: string;
hash?: string;
}
type SafeNavigateToType = string | SafeNavigateTo;
interface UseSafeNavigateReturn {
safeNavigate: jest.MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
>;
}
export const useSafeNavigate = (): UseSafeNavigateReturn => ({
safeNavigate: jest.fn(
(_to: SafeNavigateToType, _options?: SafeNavigateOptions) => {},
) as jest.MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
>,
});

View File

@@ -1,8 +1,6 @@
import type { Config } from '@jest/types';
const USE_SAFE_NAVIGATE_MOCK_PATH =
'<rootDir>/src/__tests__/safeNavigateMock.ts';
const LOG_EVENT_MOCK_PATH = '<rootDir>/src/__tests__/logEventMock.ts';
const USE_SAFE_NAVIGATE_MOCK_PATH = '<rootDir>/__mocks__/useSafeNavigate.ts';
const config: Config.InitialOptions = {
silent: true,
@@ -24,8 +22,6 @@ const config: Config.InitialOptions = {
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^api/common/logEvent$': LOG_EVENT_MOCK_PATH,
'^src/api/common/logEvent$': LOG_EVENT_MOCK_PATH,
'^constants/env$': '<rootDir>/__mocks__/env.ts',
'^src/constants/env$': '<rootDir>/__mocks__/env.ts',
'^@signozhq/icons$': '<rootDir>/__mocks__/signozhqIconsMock.tsx',

View File

@@ -351,18 +351,19 @@ function App(): JSX.Element {
Sentry.init({
dsn: process.env.SENTRY_DSN,
tunnel: process.env.TUNNEL_URL,
environment: process.env.ENVIRONMENT,
release: process.env.VERSION,
environment: 'production',
integrations: [
// Kept for the `transaction` tag used in routing, even though
// tracing is disabled. Ref: https://github.com/SigNoz/platform-pod/issues/2393#issuecomment-4603658055
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({
maskAllText: false,
blockAllMedia: false,
}),
],
tracesSampleRate: 0, // Ref: https://github.com/SigNoz/platform-pod/issues/2393#issuecomment-4603658055
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
tracePropagationTargets: [],
// Session Replay
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
beforeSend(event) {

View File

@@ -1,11 +0,0 @@
// Shared mock for `api/common/logEvent`.
// Wired into jest.config.ts moduleNameMapper, so any import of
// `api/common/logEvent` in test code resolves to this file.
// Tests can import `logEventMock` to assert analytics calls — Jest's
// `clearMocks: true` resets call history between tests.
export const logEventMock: jest.MockedFunction<
(eventName: string, attributes?: Record<string, unknown>) => void
> = jest.fn();
export default logEventMock;

View File

@@ -1,29 +0,0 @@
// Shared mock for `hooks/useSafeNavigate`.
// Wired into jest.config.ts moduleNameMapper, so any import of
// `hooks/useSafeNavigate` in test code resolves to this file.
// Tests can import `safeNavigateMock` to assert navigation calls — Jest's
// `clearMocks: true` resets call history between tests.
interface SafeNavigateOptions {
replace?: boolean;
state?: unknown;
newTab?: boolean;
}
interface SafeNavigateTo {
pathname?: string;
search?: string;
hash?: string;
}
type SafeNavigateToType = string | SafeNavigateTo;
export const safeNavigateMock: jest.MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
> = jest.fn();
export const useSafeNavigate = (): {
safeNavigate: typeof safeNavigateMock;
} => ({
safeNavigate: safeNavigateMock,
});

View File

@@ -3,36 +3,13 @@ import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface DayBreakdownEntry {
timestamp: number;
total: number;
quantity: number;
count: number;
size: number;
}
export interface TierEntry {
quantity: number;
unitPrice: number;
tierCost: number;
}
export interface BreakdownEntry {
type: string;
unit: string;
dayWiseBreakdown: {
breakdown: DayBreakdownEntry[];
};
tiers?: TierEntry[];
}
export interface UsageResponsePayloadProps {
billingPeriodStart: number;
billingPeriodEnd: number;
billingPeriodStart: Date;
billingPeriodEnd: Date;
details: {
total: number;
baseFee: number;
breakdown: BreakdownEntry[];
breakdown: [];
billTotal: number;
};
discount: number;

View File

@@ -19,7 +19,7 @@ import type {
import type {
AlertmanagertypesPostableChannelDTO,
AlertmanagertypesReceiverDTO,
ConfigReceiverDTO,
CreateChannel201,
DeleteChannelByIDPathParameters,
GetChannelByID200,
@@ -385,14 +385,14 @@ export const invalidateGetChannelByID = async (
*/
export const updateChannelByID = (
{ id }: UpdateChannelByIDPathParameters,
alertmanagertypesReceiverDTO?: BodyType<AlertmanagertypesReceiverDTO>,
configReceiverDTO?: BodyType<ConfigReceiverDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/channels/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: alertmanagertypesReceiverDTO,
data: configReceiverDTO,
signal,
});
};
@@ -406,7 +406,7 @@ export const getUpdateChannelByIDMutationOptions = <
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data?: BodyType<AlertmanagertypesReceiverDTO>;
data?: BodyType<ConfigReceiverDTO>;
},
TContext
>;
@@ -415,7 +415,7 @@ export const getUpdateChannelByIDMutationOptions = <
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data?: BodyType<AlertmanagertypesReceiverDTO>;
data?: BodyType<ConfigReceiverDTO>;
},
TContext
> => {
@@ -432,7 +432,7 @@ export const getUpdateChannelByIDMutationOptions = <
Awaited<ReturnType<typeof updateChannelByID>>,
{
pathParams: UpdateChannelByIDPathParameters;
data?: BodyType<AlertmanagertypesReceiverDTO>;
data?: BodyType<ConfigReceiverDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -447,7 +447,7 @@ export type UpdateChannelByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof updateChannelByID>>
>;
export type UpdateChannelByIDMutationBody =
| BodyType<AlertmanagertypesReceiverDTO>
| BodyType<ConfigReceiverDTO>
| undefined;
export type UpdateChannelByIDMutationError = ErrorType<RenderErrorResponseDTO>;
@@ -463,7 +463,7 @@ export const useUpdateChannelByID = <
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data?: BodyType<AlertmanagertypesReceiverDTO>;
data?: BodyType<ConfigReceiverDTO>;
},
TContext
>;
@@ -472,7 +472,7 @@ export const useUpdateChannelByID = <
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data?: BodyType<AlertmanagertypesReceiverDTO>;
data?: BodyType<ConfigReceiverDTO>;
},
TContext
> => {
@@ -483,14 +483,14 @@ export const useUpdateChannelByID = <
* @summary Test notification channel
*/
export const testChannel = (
alertmanagertypesReceiverDTO?: BodyType<AlertmanagertypesReceiverDTO>,
configReceiverDTO?: BodyType<ConfigReceiverDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/channels/test`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: alertmanagertypesReceiverDTO,
data: configReceiverDTO,
signal,
});
};
@@ -502,13 +502,13 @@ export const getTestChannelMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
{ data?: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
{ data?: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationKey = ['testChannel'];
@@ -522,7 +522,7 @@ export const getTestChannelMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof testChannel>>,
{ data?: BodyType<AlertmanagertypesReceiverDTO> }
{ data?: BodyType<ConfigReceiverDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -535,9 +535,7 @@ export const getTestChannelMutationOptions = <
export type TestChannelMutationResult = NonNullable<
Awaited<ReturnType<typeof testChannel>>
>;
export type TestChannelMutationBody =
| BodyType<AlertmanagertypesReceiverDTO>
| undefined;
export type TestChannelMutationBody = BodyType<ConfigReceiverDTO> | undefined;
export type TestChannelMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -550,13 +548,13 @@ export const useTestChannel = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
{ data?: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
{ data?: BodyType<ConfigReceiverDTO> },
TContext
> => {
return useMutation(getTestChannelMutationOptions(options));
@@ -567,14 +565,14 @@ export const useTestChannel = <
* @summary Test notification channel (deprecated)
*/
export const testChannelDeprecated = (
alertmanagertypesReceiverDTO?: BodyType<AlertmanagertypesReceiverDTO>,
configReceiverDTO?: BodyType<ConfigReceiverDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/testChannel`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: alertmanagertypesReceiverDTO,
data: configReceiverDTO,
signal,
});
};
@@ -586,13 +584,13 @@ export const getTestChannelDeprecatedMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
{ data?: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
{ data?: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationKey = ['testChannelDeprecated'];
@@ -606,7 +604,7 @@ export const getTestChannelDeprecatedMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof testChannelDeprecated>>,
{ data?: BodyType<AlertmanagertypesReceiverDTO> }
{ data?: BodyType<ConfigReceiverDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -620,7 +618,7 @@ export type TestChannelDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof testChannelDeprecated>>
>;
export type TestChannelDeprecatedMutationBody =
| BodyType<AlertmanagertypesReceiverDTO>
| BodyType<ConfigReceiverDTO>
| undefined;
export type TestChannelDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
@@ -636,13 +634,13 @@ export const useTestChannelDeprecated = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
{ data?: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
{ data?: BodyType<ConfigReceiverDTO> },
TContext
> => {
return useMutation(getTestChannelDeprecatedMutationOptions(options));

View File

@@ -31,15 +31,11 @@ import type {
DisconnectAccountPathParameters,
GetAccount200,
GetAccountPathParameters,
GetAccountService200,
GetAccountServicePathParameters,
GetConnectionCredentials200,
GetConnectionCredentialsPathParameters,
GetService200,
GetServiceParams,
GetServicePathParameters,
ListAccountServicesMetadata200,
ListAccountServicesMetadataPathParameters,
ListAccounts200,
ListAccountsPathParameters,
ListServicesMetadata200,
@@ -635,227 +631,6 @@ export const useUpdateAccount = <
> => {
return useMutation(getUpdateAccountMutationOptions(options));
};
/**
* This endpoint lists the services metadata for the specified account and cloud provider
* @summary List account services metadata
*/
export const listAccountServicesMetadata = (
{ cloudProvider, id }: ListAccountServicesMetadataPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListAccountServicesMetadata200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/accounts/${id}/services`,
method: 'GET',
signal,
});
};
export const getListAccountServicesMetadataQueryKey = ({
cloudProvider,
id,
}: ListAccountServicesMetadataPathParameters) => {
return [
`/api/v1/cloud_integrations/${cloudProvider}/accounts/${id}/services`,
] as const;
};
export const getListAccountServicesMetadataQueryOptions = <
TData = Awaited<ReturnType<typeof listAccountServicesMetadata>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ cloudProvider, id }: ListAccountServicesMetadataPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listAccountServicesMetadata>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getListAccountServicesMetadataQueryKey({ cloudProvider, id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listAccountServicesMetadata>>
> = ({ signal }) => listAccountServicesMetadata({ cloudProvider, id }, signal);
return {
queryKey,
queryFn,
enabled: !!(cloudProvider && id),
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof listAccountServicesMetadata>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListAccountServicesMetadataQueryResult = NonNullable<
Awaited<ReturnType<typeof listAccountServicesMetadata>>
>;
export type ListAccountServicesMetadataQueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary List account services metadata
*/
export function useListAccountServicesMetadata<
TData = Awaited<ReturnType<typeof listAccountServicesMetadata>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ cloudProvider, id }: ListAccountServicesMetadataPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listAccountServicesMetadata>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListAccountServicesMetadataQueryOptions(
{ cloudProvider, id },
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List account services metadata
*/
export const invalidateListAccountServicesMetadata = async (
queryClient: QueryClient,
{ cloudProvider, id }: ListAccountServicesMetadataPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListAccountServicesMetadataQueryKey({ cloudProvider, id }) },
options,
);
return queryClient;
};
/**
* This endpoint gets a service and its configuration for the specified cloud integration account
* @summary Get service for account
*/
export const getAccountService = (
{ cloudProvider, id, serviceId }: GetAccountServicePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetAccountService200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/accounts/${id}/services/${serviceId}`,
method: 'GET',
signal,
});
};
export const getGetAccountServiceQueryKey = ({
cloudProvider,
id,
serviceId,
}: GetAccountServicePathParameters) => {
return [
`/api/v1/cloud_integrations/${cloudProvider}/accounts/${id}/services/${serviceId}`,
] as const;
};
export const getGetAccountServiceQueryOptions = <
TData = Awaited<ReturnType<typeof getAccountService>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ cloudProvider, id, serviceId }: GetAccountServicePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAccountService>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetAccountServiceQueryKey({ cloudProvider, id, serviceId });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getAccountService>>
> = ({ signal }) =>
getAccountService({ cloudProvider, id, serviceId }, signal);
return {
queryKey,
queryFn,
enabled: !!(cloudProvider && id && serviceId),
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getAccountService>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetAccountServiceQueryResult = NonNullable<
Awaited<ReturnType<typeof getAccountService>>
>;
export type GetAccountServiceQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get service for account
*/
export function useGetAccountService<
TData = Awaited<ReturnType<typeof getAccountService>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ cloudProvider, id, serviceId }: GetAccountServicePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAccountService>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetAccountServiceQueryOptions(
{ cloudProvider, id, serviceId },
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get service for account
*/
export const invalidateGetAccountService = async (
queryClient: QueryClient,
{ cloudProvider, id, serviceId }: GetAccountServicePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetAccountServiceQueryKey({ cloudProvider, id, serviceId }) },
options,
);
return queryClient;
};
/**
* This endpoint updates a service for the specified cloud provider
* @summary Update service

View File

@@ -21,10 +21,8 @@ import type {
CreateDashboardV2201,
CreatePublicDashboard201,
CreatePublicDashboardPathParameters,
DashboardtypesPatchableDashboardV2DTO,
DashboardtypesPostableDashboardV2DTO,
DashboardtypesPostablePublicDashboardDTO,
DashboardtypesUpdatableDashboardV2DTO,
DashboardtypesUpdatablePublicDashboardDTO,
DeletePublicDashboardPathParameters,
GetDashboardV2200,
@@ -35,13 +33,7 @@ import type {
GetPublicDashboardPathParameters,
GetPublicDashboardWidgetQueryRange200,
GetPublicDashboardWidgetQueryRangePathParameters,
LockDashboardV2PathParameters,
PatchDashboardV2200,
PatchDashboardV2PathParameters,
RenderErrorResponseDTO,
UnlockDashboardV2PathParameters,
UpdateDashboardV2200,
UpdateDashboardV2PathParameters,
UpdatePublicDashboardPathParameters,
} from '../sigNoz.schemas';
@@ -824,360 +816,3 @@ export const invalidateGetDashboardV2 = async (
return queryClient;
};
/**
* This endpoint applies an RFC 6902 JSON Patch to a v2-shape dashboard. The patch is applied against the postable view of the dashboard (metadata, data, tags), so individual panels, queries, variables, layouts, or tags can be updated without re-sending the rest of the dashboard. Apply is lenient: `remove` on a missing path is a no-op (idempotent) and `add` creates any missing parent objects, rather than failing as strict RFC 6902 would. The resulting dashboard is still validated. Locked dashboards are rejected.
* @summary Patch dashboard (v2)
*/
export const patchDashboardV2 = (
{ id }: PatchDashboardV2PathParameters,
dashboardtypesPatchableDashboardV2DTONull?: BodyType<DashboardtypesPatchableDashboardV2DTO | null> | null,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<PatchDashboardV2200>({
url: `/api/v2/dashboards/${id}`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
data: dashboardtypesPatchableDashboardV2DTONull,
signal,
});
};
export const getPatchDashboardV2MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof patchDashboardV2>>,
TError,
{
pathParams: PatchDashboardV2PathParameters;
data?: BodyType<DashboardtypesPatchableDashboardV2DTO | null>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof patchDashboardV2>>,
TError,
{
pathParams: PatchDashboardV2PathParameters;
data?: BodyType<DashboardtypesPatchableDashboardV2DTO | null>;
},
TContext
> => {
const mutationKey = ['patchDashboardV2'];
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 patchDashboardV2>>,
{
pathParams: PatchDashboardV2PathParameters;
data?: BodyType<DashboardtypesPatchableDashboardV2DTO | null>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return patchDashboardV2(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type PatchDashboardV2MutationResult = NonNullable<
Awaited<ReturnType<typeof patchDashboardV2>>
>;
export type PatchDashboardV2MutationBody =
| BodyType<DashboardtypesPatchableDashboardV2DTO | null>
| undefined;
export type PatchDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Patch dashboard (v2)
*/
export const usePatchDashboardV2 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof patchDashboardV2>>,
TError,
{
pathParams: PatchDashboardV2PathParameters;
data?: BodyType<DashboardtypesPatchableDashboardV2DTO | null>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof patchDashboardV2>>,
TError,
{
pathParams: PatchDashboardV2PathParameters;
data?: BodyType<DashboardtypesPatchableDashboardV2DTO | null>;
},
TContext
> => {
return useMutation(getPatchDashboardV2MutationOptions(options));
};
/**
* This endpoint updates a v2-shape dashboard's metadata, data, and tag set. Locked dashboards are rejected.
* @summary Update dashboard (v2)
*/
export const updateDashboardV2 = (
{ id }: UpdateDashboardV2PathParameters,
dashboardtypesUpdatableDashboardV2DTO?: BodyType<DashboardtypesUpdatableDashboardV2DTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<UpdateDashboardV2200>({
url: `/api/v2/dashboards/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: dashboardtypesUpdatableDashboardV2DTO,
signal,
});
};
export const getUpdateDashboardV2MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateDashboardV2>>,
TError,
{
pathParams: UpdateDashboardV2PathParameters;
data?: BodyType<DashboardtypesUpdatableDashboardV2DTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateDashboardV2>>,
TError,
{
pathParams: UpdateDashboardV2PathParameters;
data?: BodyType<DashboardtypesUpdatableDashboardV2DTO>;
},
TContext
> => {
const mutationKey = ['updateDashboardV2'];
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 updateDashboardV2>>,
{
pathParams: UpdateDashboardV2PathParameters;
data?: BodyType<DashboardtypesUpdatableDashboardV2DTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateDashboardV2(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateDashboardV2MutationResult = NonNullable<
Awaited<ReturnType<typeof updateDashboardV2>>
>;
export type UpdateDashboardV2MutationBody =
| BodyType<DashboardtypesUpdatableDashboardV2DTO>
| undefined;
export type UpdateDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update dashboard (v2)
*/
export const useUpdateDashboardV2 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateDashboardV2>>,
TError,
{
pathParams: UpdateDashboardV2PathParameters;
data?: BodyType<DashboardtypesUpdatableDashboardV2DTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateDashboardV2>>,
TError,
{
pathParams: UpdateDashboardV2PathParameters;
data?: BodyType<DashboardtypesUpdatableDashboardV2DTO>;
},
TContext
> => {
return useMutation(getUpdateDashboardV2MutationOptions(options));
};
/**
* This endpoint unlocks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.
* @summary Unlock dashboard (v2)
*/
export const unlockDashboardV2 = (
{ id }: UnlockDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v2/dashboards/${id}/lock`,
method: 'DELETE',
signal,
});
};
export const getUnlockDashboardV2MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof unlockDashboardV2>>,
TError,
{ pathParams: UnlockDashboardV2PathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof unlockDashboardV2>>,
TError,
{ pathParams: UnlockDashboardV2PathParameters },
TContext
> => {
const mutationKey = ['unlockDashboardV2'];
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 unlockDashboardV2>>,
{ pathParams: UnlockDashboardV2PathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return unlockDashboardV2(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type UnlockDashboardV2MutationResult = NonNullable<
Awaited<ReturnType<typeof unlockDashboardV2>>
>;
export type UnlockDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Unlock dashboard (v2)
*/
export const useUnlockDashboardV2 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof unlockDashboardV2>>,
TError,
{ pathParams: UnlockDashboardV2PathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof unlockDashboardV2>>,
TError,
{ pathParams: UnlockDashboardV2PathParameters },
TContext
> => {
return useMutation(getUnlockDashboardV2MutationOptions(options));
};
/**
* This endpoint locks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.
* @summary Lock dashboard (v2)
*/
export const lockDashboardV2 = (
{ id }: LockDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v2/dashboards/${id}/lock`,
method: 'PUT',
signal,
});
};
export const getLockDashboardV2MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof lockDashboardV2>>,
TError,
{ pathParams: LockDashboardV2PathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof lockDashboardV2>>,
TError,
{ pathParams: LockDashboardV2PathParameters },
TContext
> => {
const mutationKey = ['lockDashboardV2'];
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 lockDashboardV2>>,
{ pathParams: LockDashboardV2PathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return lockDashboardV2(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type LockDashboardV2MutationResult = NonNullable<
Awaited<ReturnType<typeof lockDashboardV2>>
>;
export type LockDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Lock dashboard (v2)
*/
export const useLockDashboardV2 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof lockDashboardV2>>,
TError,
{ pathParams: LockDashboardV2PathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof lockDashboardV2>>,
TError,
{ pathParams: LockDashboardV2PathParameters },
TContext
> => {
return useMutation(getLockDashboardV2MutationOptions(options));
};

View File

@@ -134,6 +134,113 @@ export interface AlertmanagertypesGettableRoutePolicyDTO {
updatedBy?: string | null;
}
export enum AlertmanagertypesMaintenanceKindDTO {
fixed = 'fixed',
recurring = 'recurring',
}
export enum AlertmanagertypesMaintenanceStatusDTO {
active = 'active',
upcoming = 'upcoming',
expired = 'expired',
}
export enum AlertmanagertypesRepeatOnDTO {
sunday = 'sunday',
monday = 'monday',
tuesday = 'tuesday',
wednesday = 'wednesday',
thursday = 'thursday',
friday = 'friday',
saturday = 'saturday',
}
export enum AlertmanagertypesRepeatTypeDTO {
daily = 'daily',
weekly = 'weekly',
monthly = 'monthly',
}
export interface AlertmanagertypesRecurrenceDTO {
/**
* @type string
*/
duration: string;
/**
* @type string,null
* @format date-time
*/
endTime?: string | null;
/**
* @type array,null
*/
repeatOn?: AlertmanagertypesRepeatOnDTO[] | null;
repeatType: AlertmanagertypesRepeatTypeDTO;
/**
* @type string
* @format date-time
*/
startTime: string;
}
export interface AlertmanagertypesScheduleDTO {
/**
* @type string
* @format date-time
*/
endTime?: string;
recurrence?: AlertmanagertypesRecurrenceDTO;
/**
* @type string
* @format date-time
*/
startTime?: string;
/**
* @type string
*/
timezone: string;
}
export interface AlertmanagertypesPlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
id: string;
kind: AlertmanagertypesMaintenanceKindDTO;
/**
* @type string
*/
name: string;
schedule: AlertmanagertypesScheduleDTO;
/**
* @type string
*/
scope?: string;
status: AlertmanagertypesMaintenanceStatusDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface ConfigAuthorizationDTO {
/**
* @type string
@@ -368,130 +475,6 @@ export interface ConfigSecretURLDTO {
[key: string]: unknown;
}
export interface AlertmanagertypesGoogleChatReceiverConfigDTO {
http_config?: ConfigHTTPClientConfigDTO;
/**
* @type boolean
*/
send_resolved?: boolean;
/**
* @type string
*/
text?: string;
/**
* @type string
*/
title?: string;
webhook_url?: ConfigSecretURLDTO;
}
export enum AlertmanagertypesMaintenanceKindDTO {
fixed = 'fixed',
recurring = 'recurring',
}
export enum AlertmanagertypesMaintenanceStatusDTO {
active = 'active',
upcoming = 'upcoming',
expired = 'expired',
}
export enum AlertmanagertypesRepeatOnDTO {
sunday = 'sunday',
monday = 'monday',
tuesday = 'tuesday',
wednesday = 'wednesday',
thursday = 'thursday',
friday = 'friday',
saturday = 'saturday',
}
export enum AlertmanagertypesRepeatTypeDTO {
daily = 'daily',
weekly = 'weekly',
monthly = 'monthly',
}
export interface AlertmanagertypesRecurrenceDTO {
/**
* @type string
*/
duration: string;
/**
* @type string,null
* @format date-time
*/
endTime?: string | null;
/**
* @type array,null
*/
repeatOn?: AlertmanagertypesRepeatOnDTO[] | null;
repeatType: AlertmanagertypesRepeatTypeDTO;
/**
* @type string
* @format date-time
*/
startTime: string;
}
export interface AlertmanagertypesScheduleDTO {
/**
* @type string
* @format date-time
*/
endTime?: string;
recurrence?: AlertmanagertypesRecurrenceDTO;
/**
* @type string
* @format date-time
*/
startTime?: string;
/**
* @type string
*/
timezone: string;
}
export interface AlertmanagertypesPlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
id: string;
kind: AlertmanagertypesMaintenanceKindDTO;
/**
* @type string
*/
name: string;
schedule: AlertmanagertypesScheduleDTO;
/**
* @type string
*/
scope?: string;
status: AlertmanagertypesMaintenanceStatusDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface ConfigDiscordConfigDTO {
/**
* @type string
@@ -1651,10 +1634,6 @@ export type AlertmanagertypesPostableChannelDTO = unknown & {
* @type array
*/
email_configs?: ConfigEmailConfigDTO[];
/**
* @type array
*/
googlechat_configs?: AlertmanagertypesGoogleChatReceiverConfigDTO[];
/**
* @type array
*/
@@ -1769,89 +1748,6 @@ export interface AlertmanagertypesPostableRoutePolicyDTO {
tags?: string[] | null;
}
export interface AlertmanagertypesReceiverDTO {
/**
* @type array
*/
discord_configs?: ConfigDiscordConfigDTO[];
/**
* @type array
*/
email_configs?: ConfigEmailConfigDTO[];
/**
* @type array
*/
googlechat_configs?: AlertmanagertypesGoogleChatReceiverConfigDTO[];
/**
* @type array
*/
incidentio_configs?: ConfigIncidentioConfigDTO[];
/**
* @type array
*/
jira_configs?: ConfigJiraConfigDTO[];
/**
* @type array
*/
mattermost_configs?: ConfigMattermostConfigDTO[];
/**
* @type array
*/
msteams_configs?: ConfigMSTeamsConfigDTO[];
/**
* @type array
*/
msteamsv2_configs?: ConfigMSTeamsV2ConfigDTO[];
/**
* @type string
*/
name?: string;
/**
* @type array
*/
opsgenie_configs?: ConfigOpsGenieConfigDTO[];
/**
* @type array
*/
pagerduty_configs?: ConfigPagerdutyConfigDTO[];
/**
* @type array
*/
pushover_configs?: ConfigPushoverConfigDTO[];
/**
* @type array
*/
rocketchat_configs?: ConfigRocketchatConfigDTO[];
/**
* @type array
*/
slack_configs?: ConfigSlackConfigDTO[];
/**
* @type array
*/
sns_configs?: ConfigSNSConfigDTO[];
/**
* @type array
*/
telegram_configs?: ConfigTelegramConfigDTO[];
/**
* @type array
*/
victorops_configs?: ConfigVictorOpsConfigDTO[];
/**
* @type array
*/
webex_configs?: ConfigWebexConfigDTO[];
/**
* @type array
*/
webhook_configs?: ConfigWebhookConfigDTO[];
/**
* @type array
*/
wechat_configs?: ConfigWechatConfigDTO[];
}
export interface AuthtypesAttributeMappingDTO {
/**
* @type string
@@ -2561,6 +2457,33 @@ export interface CloudintegrationtypesAccountDTO {
updatedAt?: string;
}
export interface DashboardtypesStorableDashboardDataDTO {
[key: string]: unknown;
}
export interface CloudintegrationtypesDashboardDTO {
definition?: DashboardtypesStorableDashboardDataDTO;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
id?: string;
/**
* @type string
*/
title?: string;
}
export interface CloudintegrationtypesAssetsDTO {
/**
* @type array,null
*/
dashboards?: CloudintegrationtypesDashboardDTO[] | null;
}
export interface CloudintegrationtypesAzureConnectionArtifactDTO {
/**
* @type string
@@ -2943,54 +2866,6 @@ export interface CloudintegrationtypesPostableAgentCheckInDTO {
providerAccountId?: string;
}
export interface CloudintegrationtypesStorableIntegrationDashboardDTO {
/**
* @type string
* @format date-time
*/
createdAt: string;
/**
* @type string
*/
dashboardId: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
provider: string;
/**
* @type string
*/
slug: string;
/**
* @type string
* @format date-time
*/
updatedAt: string;
}
export interface CloudintegrationtypesServiceDashboardDTO {
/**
* @type string
*/
description: string;
integrationDashboard?: CloudintegrationtypesStorableIntegrationDashboardDTO;
/**
* @type string
*/
title: string;
}
export interface CloudintegrationtypesServiceAssetsDTO {
/**
* @type array
*/
dashboards: CloudintegrationtypesServiceDashboardDTO[];
}
export interface CloudintegrationtypesSupportedSignalsDTO {
/**
* @type boolean
@@ -3002,8 +2877,13 @@ export interface CloudintegrationtypesSupportedSignalsDTO {
metrics?: boolean;
}
export interface CloudintegrationtypesTelemetryCollectionStrategyDTO {
aws?: CloudintegrationtypesAWSTelemetryCollectionStrategyDTO;
azure?: CloudintegrationtypesAzureTelemetryCollectionStrategyDTO;
}
export interface CloudintegrationtypesServiceDTO {
assets: CloudintegrationtypesServiceAssetsDTO;
assets: CloudintegrationtypesAssetsDTO;
cloudIntegrationService: CloudintegrationtypesCloudIntegrationServiceDTO | null;
dataCollected: CloudintegrationtypesDataCollectedDTO;
/**
@@ -3019,6 +2899,7 @@ export interface CloudintegrationtypesServiceDTO {
*/
overview: string;
supportedSignals: CloudintegrationtypesSupportedSignalsDTO;
telemetryCollectionStrategy: CloudintegrationtypesTelemetryCollectionStrategyDTO;
/**
* @type string
*/
@@ -3063,6 +2944,85 @@ export interface CommonJSONRefDTO {
$ref?: string;
}
export interface ConfigReceiverDTO {
/**
* @type array
*/
discord_configs?: ConfigDiscordConfigDTO[];
/**
* @type array
*/
email_configs?: ConfigEmailConfigDTO[];
/**
* @type array
*/
incidentio_configs?: ConfigIncidentioConfigDTO[];
/**
* @type array
*/
jira_configs?: ConfigJiraConfigDTO[];
/**
* @type array
*/
mattermost_configs?: ConfigMattermostConfigDTO[];
/**
* @type array
*/
msteams_configs?: ConfigMSTeamsConfigDTO[];
/**
* @type array
*/
msteamsv2_configs?: ConfigMSTeamsV2ConfigDTO[];
/**
* @type string
*/
name?: string;
/**
* @type array
*/
opsgenie_configs?: ConfigOpsGenieConfigDTO[];
/**
* @type array
*/
pagerduty_configs?: ConfigPagerdutyConfigDTO[];
/**
* @type array
*/
pushover_configs?: ConfigPushoverConfigDTO[];
/**
* @type array
*/
rocketchat_configs?: ConfigRocketchatConfigDTO[];
/**
* @type array
*/
slack_configs?: ConfigSlackConfigDTO[];
/**
* @type array
*/
sns_configs?: ConfigSNSConfigDTO[];
/**
* @type array
*/
telegram_configs?: ConfigTelegramConfigDTO[];
/**
* @type array
*/
victorops_configs?: ConfigVictorOpsConfigDTO[];
/**
* @type array
*/
webex_configs?: ConfigWebexConfigDTO[];
/**
* @type array
*/
webhook_configs?: ConfigWebhookConfigDTO[];
/**
* @type array
*/
wechat_configs?: ConfigWechatConfigDTO[];
}
export interface CoretypesObjectGroupDTO {
resource: CoretypesResourceRefDTO;
/**
@@ -3129,40 +3089,6 @@ export interface DashboardGridLayoutSpecDTO {
repeatVariable?: string;
}
export interface DashboardLinkDTO {
/**
* @type string
*/
name?: string;
/**
* @type boolean
*/
renderVariables?: boolean;
/**
* @type boolean
*/
targetBlank?: boolean;
/**
* @type string
*/
tooltip?: string;
/**
* @type string
*/
url?: string;
}
export interface DashboardPanelDisplayDTO {
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name?: string;
}
export interface VariableDisplayDTO {
/**
* @type string
@@ -3779,10 +3705,6 @@ export interface DashboardtypesCustomVariableSpecDTO {
customValue: string;
}
export interface DashboardtypesStorableDashboardDataDTO {
[key: string]: unknown;
}
export enum DashboardtypesSourceDTO {
user = 'user',
system = 'system',
@@ -3864,9 +3786,40 @@ export type DashboardtypesDashboardSpecDTODatasources = {
[key: string]: DashboardtypesDatasourceSpecDTO;
};
export enum DashboardtypesPanelKindDTO {
Panel = 'Panel',
export interface V1PanelDisplayDTO {
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name?: string;
}
export interface V1LinkDTO {
/**
* @type string
*/
name?: string;
/**
* @type boolean
*/
renderVariables?: boolean;
/**
* @type boolean
*/
targetBlank?: boolean;
/**
* @type string
*/
tooltip?: string;
/**
* @type string
*/
url?: string;
}
export enum DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpecDTOKind {
'signoz/TimeSeriesPanel' = 'signoz/TimeSeriesPanel',
}
@@ -4108,13 +4061,6 @@ export type DashboardtypesPanelPluginDTO =
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpecDTO;
export enum Querybuildertypesv5RequestTypeDTO {
scalar = 'scalar',
time_series = 'time_series',
raw = 'raw',
raw_stream = 'raw_stream',
trace = 'trace',
}
export enum DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBuilderQuerySpecDTOKind {
'signoz/BuilderQuery' = 'signoz/BuilderQuery',
}
@@ -4419,16 +4365,19 @@ export interface DashboardtypesQuerySpecDTO {
}
export interface DashboardtypesQueryDTO {
kind?: Querybuildertypesv5RequestTypeDTO;
/**
* @type string
*/
kind?: string;
spec?: DashboardtypesQuerySpecDTO;
}
export interface DashboardtypesPanelSpecDTO {
display?: DashboardPanelDisplayDTO;
display?: V1PanelDisplayDTO;
/**
* @type array
*/
links?: DashboardLinkDTO[];
links?: V1LinkDTO[];
plugin?: DashboardtypesPanelPluginDTO;
/**
* @type array
@@ -4437,7 +4386,10 @@ export interface DashboardtypesPanelSpecDTO {
}
export interface DashboardtypesPanelDTO {
kind?: DashboardtypesPanelKindDTO;
/**
* @type string
*/
kind?: string;
spec?: DashboardtypesPanelSpecDTO;
}
@@ -4451,20 +4403,20 @@ export type DashboardtypesDashboardSpecDTOPanelsAnyOf = {
export type DashboardtypesDashboardSpecDTOPanels =
DashboardtypesDashboardSpecDTOPanelsAnyOf | null;
export enum DashboardtypesLayoutEnvelopeGithubComPersesSpecGoDashboardGridLayoutSpecDTOKind {
export enum DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpecDTOKind {
Grid = 'Grid',
}
export interface DashboardtypesLayoutEnvelopeGithubComPersesSpecGoDashboardGridLayoutSpecDTO {
export interface DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpecDTO {
/**
* @enum Grid
* @type string
*/
kind: DashboardtypesLayoutEnvelopeGithubComPersesSpecGoDashboardGridLayoutSpecDTOKind;
kind: DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpecDTOKind;
spec: DashboardGridLayoutSpecDTO;
}
export type DashboardtypesLayoutDTO =
DashboardtypesLayoutEnvelopeGithubComPersesSpecGoDashboardGridLayoutSpecDTO;
DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpecDTO;
export enum DashboardtypesVariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesListVariableSpecDTOKind {
ListVariable = 'ListVariable',
@@ -4568,21 +4520,21 @@ export interface DashboardtypesVariableEnvelopeGithubComSigNozSignozPkgTypesDash
spec: DashboardtypesListVariableSpecDTO;
}
export enum DashboardtypesVariableEnvelopeGithubComPersesSpecGoDashboardTextVariableSpecDTOKind {
export enum DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpecDTOKind {
TextVariable = 'TextVariable',
}
export interface DashboardtypesVariableEnvelopeGithubComPersesSpecGoDashboardTextVariableSpecDTO {
export interface DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpecDTO {
/**
* @enum TextVariable
* @type string
*/
kind: DashboardtypesVariableEnvelopeGithubComPersesSpecGoDashboardTextVariableSpecDTOKind;
kind: DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpecDTOKind;
spec: DashboardTextVariableSpecDTO;
}
export type DashboardtypesVariableDTO =
| DashboardtypesVariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesListVariableSpecDTO
| DashboardtypesVariableEnvelopeGithubComPersesSpecGoDashboardTextVariableSpecDTO;
| DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpecDTO;
export interface DashboardtypesDashboardSpecDTO {
/**
@@ -4601,7 +4553,7 @@ export interface DashboardtypesDashboardSpecDTO {
/**
* @type array
*/
links?: DashboardLinkDTO[];
links?: V1LinkDTO[];
/**
* @type object,null
*/
@@ -4701,32 +4653,6 @@ export interface DashboardtypesGettablePublicDashboardDataDTO {
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
}
export enum DashboardtypesPatchOpDTO {
add = 'add',
remove = 'remove',
replace = 'replace',
move = 'move',
copy = 'copy',
test = 'test',
}
export interface DashboardtypesJSONPatchOperationDTO {
/**
* @type string
* @description Source JSON Pointer for move/copy ops; ignored for other ops.
*/
from?: string;
op: DashboardtypesPatchOpDTO;
/**
* @type string
* @description JSON Pointer (RFC 6901) into the dashboard's postable shape — e.g. /spec/display/name, /spec/panels/<id>, /spec/panels/<id>/spec/queries/0, /tags/-.
*/
path: string;
/**
* @description Value to add/replace/test against. The expected type depends on the path. Common shapes (see referenced schemas for the exact field set): /spec/panels/<id> takes a DashboardtypesPanel; /spec/panels/<id>/spec/queries/N (or /-) takes a DashboardtypesQuery; /spec/variables/N takes a DashboardtypesVariable; /spec/layouts/N takes a DashboardtypesLayout; /tags/N (or /-) takes a TagtypesPostableTag; /spec/display/name and other leaf string fields take a string. Required for add/replace/test; ignored for remove/move/copy.
*/
value?: unknown;
}
export enum DashboardtypesPanelPluginKindDTO {
'signoz/TimeSeriesPanel' = 'signoz/TimeSeriesPanel',
'signoz/BarChartPanel' = 'signoz/BarChartPanel',
@@ -4736,13 +4662,6 @@ export enum DashboardtypesPanelPluginKindDTO {
'signoz/HistogramPanel' = 'signoz/HistogramPanel',
'signoz/ListPanel' = 'signoz/ListPanel',
}
/**
* @nullable
*/
export type DashboardtypesPatchableDashboardV2DTO =
| DashboardtypesJSONPatchOperationDTO[]
| null;
export interface DashboardtypesPostableDashboardV2DTO {
/**
* @type boolean
@@ -4786,26 +4705,6 @@ export enum DashboardtypesQueryPluginKindDTO {
'signoz/ClickHouseSQL' = 'signoz/ClickHouseSQL',
'signoz/TraceOperator' = 'signoz/TraceOperator',
}
export interface DashboardtypesUpdatableDashboardV2DTO {
/**
* @type string
*/
image?: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
schemaVersion: string;
spec: DashboardtypesDashboardSpecDTO;
/**
* @type array,null
*/
tags: TagtypesPostableTagDTO[] | null;
}
export interface DashboardtypesUpdatablePublicDashboardDTO {
/**
* @type string
@@ -6983,6 +6882,13 @@ export type Querybuildertypesv5QueryRangeRequestDTOVariables = {
[key: string]: Querybuildertypesv5VariableItemDTO;
};
export enum Querybuildertypesv5RequestTypeDTO {
scalar = 'scalar',
time_series = 'time_series',
raw = 'raw',
raw_stream = 'raw_stream',
trace = 'trace',
}
/**
* Request body for the v5 query range endpoint. Supports builder queries (traces, logs, metrics), formulas, joins, trace operators, PromQL, and ClickHouse SQL queries.
*/
@@ -7769,6 +7675,81 @@ export enum SpantypesFieldContextDTO {
attribute = 'attribute',
resource = 'resource',
}
export type SpantypesFlamegraphSpanDTOAttributes = { [key: string]: unknown };
export type SpantypesFlamegraphSpanDTOResource = { [key: string]: string };
export interface SpantypesFlamegraphSpanDTO {
/**
* @type object
*/
attributes?: SpantypesFlamegraphSpanDTOAttributes;
/**
* @type integer
* @minimum 0
*/
durationNano?: number;
/**
* @type array,null
*/
event?: SpantypesEventDTO[] | null;
/**
* @type boolean
*/
hasError?: boolean;
/**
* @type integer
* @format int64
*/
level?: number;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
parentSpanId?: string;
/**
* @type object
*/
resource?: SpantypesFlamegraphSpanDTOResource;
/**
* @type string
*/
serviceName?: string;
/**
* @type string
*/
spanId?: string;
/**
* @type integer
* @minimum 0
*/
timestamp?: number;
}
export interface SpantypesGettableFlamegraphTraceDTO {
/**
* @type integer
* @format int64
*/
endTimestampMillis?: number;
/**
* @type boolean
*/
hasMore?: boolean;
/**
* @type array,null
*/
spans?: SpantypesFlamegraphSpanDTO[][] | null;
/**
* @type integer
* @format int64
*/
startTimestampMillis?: number;
}
export type SpantypesSpanMapperGroupConditionDTOAnyOf = {
/**
* @type array,null
@@ -8070,6 +8051,17 @@ export interface SpantypesGettableWaterfallTraceDTO {
uncollapsedSpans?: string[] | null;
}
export interface SpantypesPostableFlamegraphDTO {
/**
* @type array
*/
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[];
/**
* @type string
*/
selectedSpanId?: string;
}
export enum SpantypesSpanMapperOperationDTO {
move = 'move',
copy = 'copy',
@@ -8717,31 +8709,6 @@ export type UpdateAccountPathParameters = {
cloudProvider: string;
id: string;
};
export type ListAccountServicesMetadataPathParameters = {
cloudProvider: string;
id: string;
};
export type ListAccountServicesMetadata200 = {
data: CloudintegrationtypesGettableServicesMetadataDTO;
/**
* @type string
*/
status: string;
};
export type GetAccountServicePathParameters = {
cloudProvider: string;
id: string;
serviceId: string;
};
export type GetAccountService200 = {
data: CloudintegrationtypesServiceDTO;
/**
* @type string
*/
status: string;
};
export type UpdateServicePathParameters = {
cloudProvider: string;
id: string;
@@ -9595,34 +9562,6 @@ export type GetDashboardV2200 = {
status: string;
};
export type PatchDashboardV2PathParameters = {
id: string;
};
export type PatchDashboardV2200 = {
data: DashboardtypesGettableDashboardV2DTO;
/**
* @type string
*/
status: string;
};
export type UpdateDashboardV2PathParameters = {
id: string;
};
export type UpdateDashboardV2200 = {
data: DashboardtypesGettableDashboardV2DTO;
/**
* @type string
*/
status: string;
};
export type UnlockDashboardV2PathParameters = {
id: string;
};
export type LockDashboardV2PathParameters = {
id: string;
};
export type GetFeatures200 = {
/**
* @type array
@@ -10424,6 +10363,17 @@ export type GetHosts200 = {
status: string;
};
export type GetFlamegraphPathParameters = {
traceID: string;
};
export type GetFlamegraph200 = {
data: SpantypesGettableFlamegraphTraceDTO;
/**
* @type string
*/
status: string;
};
export type GetWaterfallPathParameters = {
traceID: string;
};

View File

@@ -12,6 +12,8 @@ import type {
} from 'react-query';
import type {
GetFlamegraph200,
GetFlamegraphPathParameters,
GetTraceAggregations200,
GetTraceAggregationsPathParameters,
GetWaterfall200,
@@ -19,6 +21,7 @@ import type {
GetWaterfallV4200,
GetWaterfallV4PathParameters,
RenderErrorResponseDTO,
SpantypesPostableFlamegraphDTO,
SpantypesPostableTraceAggregationsDTO,
SpantypesPostableWaterfallDTO,
} from '../sigNoz.schemas';
@@ -126,6 +129,105 @@ export const useGetTraceAggregations = <
> => {
return useMutation(getGetTraceAggregationsMutationOptions(options));
};
/**
* 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

@@ -120,8 +120,7 @@ export const interceptorRejected = async (
!(
response.config.url === '/sessions' && response.config.method === 'delete'
) &&
response.config.url !== '/authz/check' &&
response.config.url !== '/api/v2/reset_password_tokens/verify'
response.config.url !== '/authz/check'
) {
try {
const accessToken = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN);

View File

@@ -1,14 +1,15 @@
import { getWaterfallV4 } from 'api/generated/services/tracedetail';
import { ApiV3Instance as axios } from 'api';
import { omit } from 'lodash-es';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
GetTraceV4PayloadProps,
GetTraceV4SuccessResponse,
GetTraceV3PayloadProps,
GetTraceV3SuccessResponse,
SpanV3,
} from 'types/api/trace/getTraceV3';
const getTraceV4 = async (
props: GetTraceV4PayloadProps,
): Promise<SuccessResponse<GetTraceV4SuccessResponse> | ErrorResponse> => {
const getTraceV3 = async (
props: GetTraceV3PayloadProps,
): Promise<SuccessResponse<GetTraceV3SuccessResponse> | ErrorResponse> => {
let uncollapsedSpans = [...props.uncollapsedSpans];
if (!props.isSelectedSpanIDUnCollapsed) {
uncollapsedSpans = uncollapsedSpans.filter(
@@ -18,37 +19,31 @@ const getTraceV4 = async (
props.selectedSpanId &&
!uncollapsedSpans.includes(props.selectedSpanId)
) {
// Backend only uses the uncollapsedSpans list (unlike V2 which also interprets
// V3 backend only uses uncollapsedSpans list (unlike V2 which also interprets
// isSelectedSpanIDUnCollapsed server-side), so explicitly add the selected span
uncollapsedSpans.push(props.selectedSpanId);
}
const response = await getWaterfallV4(
{ traceID: props.traceId },
{
selectedSpanId: props.selectedSpanId,
uncollapsedSpans,
limit: 10000,
},
const postData: GetTraceV3PayloadProps = {
...props,
uncollapsedSpans,
limit: 10000,
};
const response = await axios.post<GetTraceV3SuccessResponse>(
`/traces/${props.traceId}/waterfall`,
omit(postData, 'traceId'),
);
// Generated client unwraps the axios response; .data is the waterfall payload.
// Wire spans carry time_unix; SpanV3's timestamp + 'service.name' are derived below.
type WireSpan = Omit<SpanV3, 'timestamp' | 'service.name'> & {
time_unix: number;
};
const rawPayload = response.data as unknown as Omit<
GetTraceV4SuccessResponse,
'spans'
> & { spans: WireSpan[] | null };
// V3 API wraps response in { status, data }
const rawPayload = (response.data as any).data || response.data;
// Derive 'service.name' from resource for convenience — only derived field
const spans: SpanV3[] = (rawPayload.spans || []).map((span) => ({
const spans: SpanV3[] = (rawPayload.spans || []).map((span: any) => ({
...span,
'service.name': span.resource?.['service.name'] || '',
timestamp: span.time_unix,
}));
// API returns startTimestampMillis/endTimestampMillis as relative durations (ms from epoch offset),
// V3 API returns startTimestampMillis/endTimestampMillis as relative durations (ms from epoch offset),
// not absolute unix millis like V2. The span timestamps are absolute unix millis.
// Convert by using the first span's timestamp as the base if there's a mismatch.
let { startTimestampMillis, endTimestampMillis } = rawPayload;
@@ -75,4 +70,4 @@ const getTraceV4 = async (
};
};
export default getTraceV4;
export default getTraceV3;

View File

@@ -349,7 +349,7 @@ function convertV5DataByType(
*/
// eslint-disable-next-line sonarjs/cognitive-complexity
export function convertV5ResponseToLegacy(
v5Response: SuccessResponse<MetricRangePayloadV5, QueryRangeRequestV5>,
v5Response: SuccessResponse<MetricRangePayloadV5>,
legendMap: Record<string, string>,
formatForWeb?: boolean,
): SuccessResponse<MetricRangePayloadV3> & { warning?: Warning } {
@@ -357,7 +357,7 @@ export function convertV5ResponseToLegacy(
const v5Data = payload?.data;
const aggregationPerQuery =
params?.compositeQuery?.queries
(params as QueryRangeRequestV5)?.compositeQuery?.queries
?.filter((query) => query.type === 'builder_query')
.reduce(
(acc, query) => {

View File

@@ -0,0 +1 @@
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#prefix__clip0_4062_7291)" stroke-width="1.167" stroke-linecap="round" stroke-linejoin="round"><path d="M7 12.833A5.833 5.833 0 107 1.167a5.833 5.833 0 000 11.666z" fill="#E5484D" stroke="#E5484D"/><path d="M8.75 5.25l-3.5 3.5M5.25 5.25l3.5 3.5" stroke="#121317"/></g><defs><clipPath id="prefix__clip0_4062_7291"><path fill="#fff" d="M0 0h14v14H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@@ -359,7 +359,8 @@ function CustomTimePickerPopoverContent({
<Clock
color={Color.BG_ROBIN_400}
className="timezone-container__clock-icon"
size={14}
height={12}
width={12}
/>
<span className="timezone__name">{timezone.name}</span>

View File

@@ -27,15 +27,13 @@ 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.key as string });
useSortable({ id: field.name });
const style = {
transform: CSS.Transform.toString(transform),
@@ -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,18 +78,14 @@ function AddedFields({
fields,
onFieldsChange,
maxFields,
requiredFields = [],
}: AddedFieldsProps): JSX.Element {
const sensors = useSensors(useSensor(PointerSensor));
// Contract: caller (FieldsSelector) normalizes `fields` so every entry has
// `.key` populated. AddedFields reads it directly.
const handleDragEnd = (event: DragEndEvent): void => {
const { active, over } = event;
if (over && active.id !== over.id) {
const oldIndex = fields.findIndex((f) => f.key === active.id);
const newIndex = fields.findIndex((f) => f.key === over.id);
const oldIndex = fields.findIndex((f) => f.name === active.id);
const newIndex = fields.findIndex((f) => f.name === over.id);
onFieldsChange(arrayMove(fields, oldIndex, newIndex));
}
};
@@ -108,7 +99,7 @@ function AddedFields({
);
const handleRemove = (field: TelemetryFieldKey): void => {
onFieldsChange(fields.filter((f) => f.key !== field.key));
onFieldsChange(fields.filter((f) => f.name !== field.name));
};
const allowDrag = inputValue.length === 0;
@@ -134,17 +125,16 @@ function AddedFields({
<div className={styles.noValues}>No values found</div>
) : (
<SortableContext
items={fields.map((f) => f.key as string)}
items={fields.map((f) => f.name)}
strategy={verticalListSortingStrategy}
disabled={!allowDrag}
>
{filteredFields.map((field) => (
<SortableField
key={field.key}
key={field.name}
field={field}
onRemove={handleRemove}
allowDrag={allowDrag}
isRequired={requiredFields.includes(field.key as string)}
/>
))}
</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

@@ -5,7 +5,6 @@ import { Input } from '@signozhq/ui/input';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
import { FloatingPanel } from 'periscope/components/FloatingPanel';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
@@ -27,36 +26,33 @@ interface FieldsSelectorProps {
onClose: () => void;
signal: DataSource;
maxFields?: number;
requiredFields?: readonly string[];
width?: number;
height?: number;
defaultPosition?: { x: number; y: number };
}
type FieldsSelectorContentProps = Omit<FieldsSelectorProps, 'isOpen'>;
// Inner component: holds all hooks + UI. Gets mounted/unmounted via the
// outer gate so opening always seeds a fresh draft from `fields`.
// Assumes `fields` arrives normalized (key populated) — see outer gate.
function FieldsSelectorContent({
function FieldsSelector({
isOpen,
title,
fields,
onFieldsChange,
onClose,
signal,
maxFields,
requiredFields,
width = DEFAULT_PANEL_WIDTH,
height,
defaultPosition,
}: FieldsSelectorContentProps): JSX.Element {
}: FieldsSelectorProps): JSX.Element | null {
if (!isOpen) {
return null;
}
const resolvedHeight =
height ?? window.innerHeight - DEFAULT_PANEL_HEIGHT_OFFSET;
const resolvedPosition = defaultPosition ?? {
x: window.innerWidth - width - DEFAULT_PANEL_RIGHT_INSET,
y: DEFAULT_PANEL_TOP_INSET,
};
const [draftFields, setDraftFields] = useState<TelemetryFieldKey[]>(fields);
const [inputValue, setInputValue] = useState('');
const [debouncedInputValue, setDebouncedInputValue] = useState('');
@@ -76,17 +72,15 @@ function FieldsSelectorContent({
const handleAdd = useCallback(
(field: TelemetryFieldKey): void => {
setDraftFields((prev) => {
if (maxFields !== undefined && prev.length >= maxFields) {
return prev;
}
if (prev.some((f) => f.key === field.key)) {
return prev;
}
return [...prev, field];
});
if (maxFields !== undefined && draftFields.length >= maxFields) {
return;
}
if (draftFields.some((f) => f.name === field.name)) {
return;
}
setDraftFields((prev) => [...prev, field]);
},
[maxFields],
[draftFields, maxFields],
);
const handleSave = useCallback((): void => {
@@ -105,7 +99,7 @@ function FieldsSelectorContent({
() =>
!(
draftFields.length === fields.length &&
draftFields.every((f, i) => f.key === fields[i]?.key)
draftFields.every((f, i) => f.name === fields[i]?.name)
),
[draftFields, fields],
);
@@ -144,7 +138,6 @@ function FieldsSelectorContent({
fields={draftFields}
onFieldsChange={setDraftFields}
maxFields={maxFields}
requiredFields={requiredFields}
/>
<OtherFields
@@ -180,27 +173,4 @@ function FieldsSelectorContent({
);
}
// Outer gate: normalizes `fields` once (populates `key` so downstream code
// can read it directly) and decides whether the inner component renders.
// When isOpen flips false→true, the inner remounts → draft state seeds fresh.
function FieldsSelector({
isOpen,
fields,
...rest
}: FieldsSelectorProps): JSX.Element | null {
const normalizedFields = useMemo<TelemetryFieldKey[]>(
() =>
fields.map((f) => ({
...f,
key: f.key ?? buildCompositeKey(f.name, f.fieldContext),
})),
[fields],
);
if (!isOpen) {
return null;
}
return <FieldsSelectorContent {...rest} fields={normalizedFields} />;
}
export default FieldsSelector;

View File

@@ -4,7 +4,6 @@ import { Skeleton } from 'antd';
import cx from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import {
FieldContext,
@@ -48,22 +47,15 @@ function OtherFields({
const otherFields: TelemetryFieldKey[] = useMemo(() => {
const suggestions = Object.values(data?.data.data.keys || {}).flat();
// Normalize: synthesize `key` once so downstream reads can trust it.
const normalizedSuggestions: TelemetryFieldKey[] = suggestions.map(
(attr) => ({
const addedNames = new Set(addedFields.map((f) => f.name));
return suggestions
.filter((attr) => !addedNames.has(attr.name))
.map((attr) => ({
...attr,
key: buildCompositeKey(attr.name, attr.fieldContext as string),
signal: attr.signal as SignalType,
fieldContext: attr.fieldContext as FieldContext,
fieldDataType: attr.fieldDataType as FieldDataType,
}),
);
const addedIds = new Set(
addedFields.map((f) => f.key ?? buildCompositeKey(f.name, f.fieldContext)),
);
return normalizedSuggestions.filter(
(attr) => !addedIds.has(attr.key as string),
);
}));
}, [data, addedFields]);
if (isFetching) {
@@ -72,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>
@@ -96,7 +83,7 @@ function OtherFields({
) : (
otherFields.map((attr) => (
<div
key={attr.key}
key={attr.name}
className={cx(styles.fieldItem, styles.otherFieldItem)}
>
<span className={styles.fieldKey}>{attr.name}</span>

View File

@@ -1,111 +0,0 @@
import { render, screen } from 'tests/test-utils';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import AddedFields from '../AddedFields';
// AddedFields assumes the caller has populated `key` (the parent
// FieldsSelector does this via its normalization useMemo). Tests pre-populate
// it directly.
const makeField = (name: string, fieldContext = 'log'): TelemetryFieldKey => ({
name,
signal: 'logs',
fieldContext: fieldContext as TelemetryFieldKey['fieldContext'],
fieldDataType: 'string',
key: `${fieldContext}.${name}`,
});
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 whose composite key is in requiredFields', () => {
const fields = [makeField('a'), makeField('b'), makeField('c')];
render(
<AddedFields
inputValue=""
fields={fields}
onFieldsChange={jest.fn()}
requiredFields={['log.a', 'log.c']}
/>,
);
// Only 'b' is removable.
const removeButtons = screen.getAllByRole('button', { name: /remove/i });
expect(removeButtons).toHaveLength(1);
});
it('still renders the field name for required fields', () => {
const fields = [makeField('a'), makeField('b')];
render(
<AddedFields
inputValue=""
fields={fields}
onFieldsChange={jest.fn()}
requiredFields={['log.a']}
/>,
);
expect(screen.getByText('a')).toBeInTheDocument();
expect(screen.getByText('b')).toBeInTheDocument();
});
it('locks ONLY the canonical variant — a same-name field from another context stays removable', () => {
// Two `body` fields with different contexts. requiredFields holds the
// canonical composite key only, so the attribute variant is deletable.
const fields = [makeField('body', 'log'), makeField('body', 'attribute')];
render(
<AddedFields
inputValue=""
fields={fields}
onFieldsChange={jest.fn()}
requiredFields={['log.body']}
/>,
);
// One Remove button: the attribute variant. log variant is locked.
expect(screen.getAllByRole('button', { name: /remove/i })).toHaveLength(1);
});
it('does not lock anything when a bare name is passed (composite key required)', () => {
// Bare `body` no longer matches — matching is composite-key only now.
const fields = [makeField('body', 'log'), makeField('body', 'attribute')];
render(
<AddedFields
inputValue=""
fields={fields}
onFieldsChange={jest.fn()}
requiredFields={['body']}
/>,
);
// Neither variant locked → both removable.
expect(screen.getAllByRole('button', { name: /remove/i })).toHaveLength(2);
});
it('treats requiredFields as exact composite-key match (substring does not lock)', () => {
const fields = [makeField('body'), makeField('body_extra')];
render(
<AddedFields
inputValue=""
fields={fields}
onFieldsChange={jest.fn()}
requiredFields={['log.body']}
/>,
);
// 'log.body' locked, 'log.body_extra' removable.
expect(screen.getAllByRole('button', { name: /remove/i })).toHaveLength(1);
});
});

View File

@@ -3,12 +3,17 @@ import { useLocation } from 'react-router-dom';
import { toast } from '@signozhq/ui/sonner';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { logEventMock } from '__tests__/logEventMock';
import logEvent from 'api/common/logEvent';
import { handleContactSupport } from 'container/Integrations/utils';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import FeedbackModal from '../FeedbackModal';
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(() => Promise.resolve()),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
@@ -30,6 +35,7 @@ jest.mock('container/Integrations/utils', () => ({
handleContactSupport: jest.fn(),
}));
const mockLogEvent = logEvent as jest.MockedFunction<typeof logEvent>;
const mockUseLocation = useLocation as jest.Mock;
const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock;
const mockHandleContactSupport = handleContactSupport as jest.Mock;
@@ -44,7 +50,6 @@ const mockLocation = {
describe('FeedbackModal', () => {
beforeEach(() => {
jest.clearAllMocks();
logEventMock.mockReturnValue(Promise.resolve() as never);
mockUseLocation.mockReturnValue(mockLocation);
mockUseGetTenantLicense.mockReturnValue({
isCloudUser: false,
@@ -111,7 +116,7 @@ describe('FeedbackModal', () => {
await user.type(textarea, testFeedback);
await user.click(submitButton);
expect(logEventMock).toHaveBeenCalledWith('Feedback: Submitted', {
expect(mockLogEvent).toHaveBeenCalledWith('Feedback: Submitted', {
data: testFeedback,
type: 'feedback',
page: mockLocation.pathname,
@@ -144,7 +149,7 @@ describe('FeedbackModal', () => {
await user.type(textarea, testFeedback);
await user.click(submitButton);
expect(logEventMock).toHaveBeenCalledWith('Feedback: Submitted', {
expect(mockLogEvent).toHaveBeenCalledWith('Feedback: Submitted', {
data: testFeedback,
type: 'reportBug',
page: mockLocation.pathname,
@@ -177,7 +182,7 @@ describe('FeedbackModal', () => {
await user.type(textarea, testFeedback);
await user.click(submitButton);
expect(logEventMock).toHaveBeenCalledWith('Feedback: Submitted', {
expect(mockLogEvent).toHaveBeenCalledWith('Feedback: Submitted', {
data: testFeedback,
type: 'featureRequest',
page: mockLocation.pathname,

View File

@@ -2,11 +2,16 @@
import { useLocation } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { logEventMock } from '__tests__/logEventMock';
import logEvent from 'api/common/logEvent';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import HeaderRightSection from '../HeaderRightSection';
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
@@ -45,6 +50,7 @@ jest.mock('hooks/useIsAIAssistantEnabled', () => ({
useIsAIAssistantEnabled: (): boolean => false,
}));
const mockLogEvent = logEvent as jest.Mock;
const mockUseLocation = useLocation as jest.Mock;
const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock;
@@ -114,7 +120,7 @@ describe('HeaderRightSection', () => {
await user.click(feedbackButton!);
expect(logEventMock).toHaveBeenCalledWith('Feedback: Clicked', {
expect(mockLogEvent).toHaveBeenCalledWith('Feedback: Clicked', {
page: mockLocation.pathname,
});
expect(screen.getByTestId('feedback-modal')).toBeInTheDocument();
@@ -127,7 +133,7 @@ describe('HeaderRightSection', () => {
const shareButton = screen.getByRole('button', { name: /share/i });
await user.click(shareButton);
expect(logEventMock).toHaveBeenCalledWith('Share: Clicked', {
expect(mockLogEvent).toHaveBeenCalledWith('Share: Clicked', {
page: mockLocation.pathname,
});
expect(screen.getByTestId('share-modal')).toBeInTheDocument();
@@ -144,7 +150,7 @@ describe('HeaderRightSection', () => {
await user.click(announcementsButton!);
expect(logEventMock).toHaveBeenCalledWith('Announcements: Clicked', {
expect(mockLogEvent).toHaveBeenCalledWith('Announcements: Clicked', {
page: mockLocation.pathname,
});
});

View File

@@ -5,13 +5,18 @@ import { matchPath, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { logEventMock } from '__tests__/logEventMock';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
import ShareURLModal from '../ShareURLModal';
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
@@ -48,6 +53,7 @@ Object.defineProperty(window, 'location', {
writable: true,
});
const mockLogEvent = logEvent as jest.Mock;
const mockUseLocation = useLocation as jest.Mock;
const mockUseUrlQuery = useUrlQuery as jest.Mock;
const mockUseSelector = useSelector as jest.Mock;
@@ -119,7 +125,7 @@ describe('ShareURLModal', () => {
await user.click(copyButton);
expect(mockHandleCopyToClipboard).toHaveBeenCalled();
expect(logEventMock).toHaveBeenCalledWith('Share: Copy link clicked', {
expect(mockLogEvent).toHaveBeenCalledWith('Share: Copy link clicked', {
page: TEST_PATH,
URL: expect.any(String),
});

View File

@@ -1,145 +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, type = ''): 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', 'log'),
field('request.id'),
field('timestamp', 'log'),
],
fontSize: FontSize.SMALL,
}),
);
// body/timestamp appear where the caller placed them, keyed by their
// composite IDs ('log.*'); contextless user fields collapse to bare name.
expect(result.current.map((c) => c.id)).toStrictEqual([
'state-indicator',
'service.name',
'log.body',
'request.id',
'log.timestamp',
]);
});
it('renders a same-name field from another context as a DISTINCT column (no collision)', () => {
const { result } = renderHook(() =>
useLogsTableColumns({
fields: [field('body', 'log'), field('body', 'attribute')],
fontSize: FontSize.SMALL,
}),
);
const byId = new Map(result.current.map((c) => [c.id, c]));
// Attribute variant is its own column, not a duplicate 'log.body'.
expect(result.current.map((c) => c.id)).toStrictEqual([
'state-indicator',
'log.body',
'attribute.body',
]);
expect(byId.get('log.body')?.enableRemove).toBe(false);
expect(byId.get('attribute.body')?.enableRemove).toBe(true);
});
it('applies the same distinct-column treatment to timestamp variants', () => {
const { result } = renderHook(() =>
useLogsTableColumns({
fields: [field('timestamp', 'log'), field('timestamp', 'attribute')],
fontSize: FontSize.SMALL,
}),
);
const byId = new Map(result.current.map((c) => [c.id, c]));
expect(result.current.map((c) => c.id)).toStrictEqual([
'state-indicator',
'log.timestamp',
'attribute.timestamp',
]);
expect(byId.get('log.timestamp')?.enableRemove).toBe(false);
expect(byId.get('attribute.timestamp')?.enableRemove).toBe(true);
});
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', 'log'),
field('timestamp', 'log'),
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('log.body')?.canBeHidden).toBe(false);
expect(byId.get('log.body')?.enableRemove).toBe(false);
expect(byId.get('log.timestamp')?.canBeHidden).toBe(false);
expect(byId.get('log.timestamp')?.enableRemove).toBe(false);
// User-added fields stay removable. User field has type='' so composite
// collapses to bare name.
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

@@ -7,7 +7,6 @@ import {
getSanitizedLogBody,
} from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import { FlatLogData } from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
import { IField } from 'types/api/logs/fields';
@@ -19,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();
@@ -46,74 +47,74 @@ export function useLogsTableColumns({
),
};
const timestampCol: TableColumnDef<ILog> = {
id: buildCompositeKey('timestamp', 'log'),
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: buildCompositeKey('body', 'log'),
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: buildCompositeKey(f.name, f.type),
header: f.name,
accessorFn: (log): unknown => FlatLogData(log)[f.name],
enableRemove: true,
width: { min: 192 },
cell: ({ value }): ReactElement => (
<TanStackTable.Text>{String(value ?? '')}</TanStackTable.Text>
),
});
// Match body/timestamp by composite key, not bare name — else a variant
// like `attribute.body` collapses onto `log.body`, duplicating the column.
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,
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 compositeKey = buildCompositeKey(f.name, f.type);
if (compositeKey === timestampCol.id) {
return timestampCol;
}
if (compositeKey === bodyCol.id) {
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,
enableRemove: 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,12 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Button, 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 +14,7 @@ import {
Minus,
Plus,
SlidersVertical,
X,
} from '@signozhq/icons';
import './LogsFormatOptionsMenu.styles.scss';
@@ -19,21 +23,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 +40,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 +61,7 @@ function OptionsMenu({
const handleMenuItemClick = (key: LogViewMode): void => {
setSelectedItem(key);
onChange(key);
setShowAddNewColumnContainer(false);
};
const incrementMaxLinesPerRow = (): void => {
@@ -71,6 +76,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 +100,6 @@ function OptionsMenu({
}
};
const handleEditColumns = (): void => {
onOpenColumns?.();
closePopover();
};
useEffect(() => {
if (maxLinesPerRow && config && config.maxLines?.onChange) {
config.maxLines.onChange(maxLinesPerRow);
@@ -98,10 +112,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 +269,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 +373,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 +452,6 @@ function LogsFormatOptionsMenu({
items,
selectedOptionFormat,
config,
onOpenColumns,
}: LogsFormatOptionsMenuProps): JSX.Element {
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
return (
@@ -264,8 +461,6 @@ function LogsFormatOptionsMenu({
items={items}
selectedOptionFormat={selectedOptionFormat}
config={config}
onOpenColumns={onOpenColumns}
closePopover={(): void => setIsPopoverOpen(false)}
/>
}
trigger="click"

View File

@@ -155,66 +155,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

@@ -109,16 +109,6 @@ $custom-border-color: #2c3044;
color: color-mix(in srgb, var(--l2-foreground) 45%, transparent);
}
.ant-select-clear {
background-color: var(--l2-background);
color: var(--l2-foreground);
font-size: 12px;
&:hover {
color: var(--l1-foreground);
}
}
// Customize tags in multiselect (dark mode by default)
.ant-select-selection-item {
background-color: var(--l1-border);
@@ -402,9 +392,7 @@ $custom-border-color: #2c3044;
// Custom dropdown styles for multi-select
.custom-multiselect-dropdown {
padding: 8px 0 0 0;
// Tall enough to hold the react-virtuoso list (<=300px) + header/footer
// so only the list scrolls (avoids a second scrollbar on this container).
max-height: 410px;
max-height: 350px;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
@@ -495,12 +483,8 @@ $custom-border-color: #2c3044;
.option-checkbox {
width: 100%;
// @signozhq/ui Checkbox renders children inside a <label> that is
// content-sized by default. Make it fill the row (min-width: 0 lets it
// shrink) so the option text below can truncate instead of overflowing.
> label {
flex: 1 1 auto;
min-width: 0;
> span:not(.ant-checkbox) {
width: 100%;
}
.all-option-text {
@@ -517,12 +501,7 @@ $custom-border-color: #2c3044;
width: 100%;
.option-label-text {
flex: 1 1 auto;
min-width: 0;
margin-bottom: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.option-badge {
@@ -535,30 +514,26 @@ $custom-border-color: #2c3044;
}
}
// Size the buttons to the row's resting content height (20px) and fully
// override antd's default 32px Button box, so revealing them on hover
// never changes the row height.
.only-btn,
.only-btn {
display: none;
}
.toggle-btn {
display: none;
align-items: center;
justify-content: center;
height: 18px;
min-height: 0;
padding: 0 6px;
font-size: 12px;
line-height: 1;
border: none;
box-shadow: none;
}
&:hover {
background-color: unset;
}
.only-btn:hover {
background-color: unset;
}
.toggle-btn:hover {
background-color: unset;
}
.option-content:hover {
.only-btn {
display: flex;
align-items: center;
justify-content: center;
height: 21px;
}
.toggle-btn {
display: none;
@@ -573,6 +548,9 @@ $custom-border-color: #2c3044;
.option-checkbox:hover {
.toggle-btn {
display: flex;
align-items: center;
justify-content: center;
height: 21px;
}
.option-badge {
display: none;

View File

@@ -721,53 +721,6 @@ export const removeKeysFromExpression = (
return result?.text ?? '';
};
const escapeRegExp = (value: string): string =>
value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
export const createVariablePlaceholderRegExp = (
variableName: string,
): RegExp => {
const escapedName = escapeRegExp(variableName);
// (?![\w.]) prevents $env from matching inside $environment or $env.attr
return new RegExp(
`(\\$${escapedName}(?![\\w.])|\\{\\{\\s*\\.?${escapedName}\\s*\\}\\}|\\[\\[\\s*${escapedName}\\s*\\]\\])`,
'g',
);
};
const matchesVariablePlaceholder = (
text: string,
variableName: string,
): boolean => createVariablePlaceholderRegExp(variableName).test(text);
export const removeVariableFromExpression = (
expression: string | undefined,
variableName: string,
): string => {
if (!expression) {
return '';
}
const queryPairs = extractQueryPairs(expression);
const keysToRemove = queryPairs
.filter((pair) => {
const singleValue = pair.value?.toString() ?? '';
const listValues = (pair.valueList ?? []).join(' ');
return (
matchesVariablePlaceholder(singleValue, variableName) ||
matchesVariablePlaceholder(listValues, variableName)
);
})
.map((pair) => pair.key);
if (keysToRemove.length === 0) {
return expression;
}
return removeKeysFromExpression(expression, keysToRemove, `$${variableName}`);
};
/**
* Convert old having format to new having format
* @param having - Array of old having objects with columnName, op, and value

View File

@@ -1,17 +1,17 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Tooltip } from 'antd';
import refreshPaymentStatus from 'api/v3/licenses/put';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import cx from 'classnames';
import { RefreshCcw } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
function RefreshPaymentStatus({
btnShape,
type,
className,
}: {
btnShape?: 'default' | 'round' | 'circle';
type?: 'button' | 'text' | 'tooltip';
className?: string;
}): JSX.Element {
const { t } = useTranslation(['failedPayment']);
const { activeLicenseRefetch } = useAppContext();
@@ -31,33 +31,26 @@ function RefreshPaymentStatus({
setIsLoading(false);
};
const button = (
<Button
variant="link"
color={type === 'text' ? 'none' : 'secondary'}
size="md"
className={className}
onClick={handleRefreshPaymentStatus}
prefix={<RefreshCcw size={14} />}
loading={isLoading}
>
{type !== 'tooltip' ? t('refreshPaymentStatus') : ''}
</Button>
);
return (
<span className="refresh-payment-status-btn-wrapper">
{type === 'tooltip' ? (
<TooltipSimple title={t('refreshPaymentStatus')}>{button}</TooltipSimple>
) : (
button
)}
<Tooltip title={type === 'tooltip' ? t('refreshPaymentStatus') : ''}>
<Button
type={type === 'text' ? 'text' : 'default'}
shape={btnShape}
className={cx('periscope-btn', { text: type === 'text' })}
onClick={handleRefreshPaymentStatus}
icon={<RefreshCcw size={14} />}
loading={isLoading}
>
{type !== 'tooltip' ? t('refreshPaymentStatus') : ''}
</Button>
</Tooltip>
</span>
);
}
RefreshPaymentStatus.defaultProps = {
btnShape: 'default',
type: 'button',
className: undefined,
};
export default RefreshPaymentStatus;

View File

@@ -72,7 +72,6 @@ function TanStackTableInner<TData>(
data,
columns,
columnStorageKey,
respectColumnOrder = true,
columnSizing: columnSizingProp,
onColumnSizingChange,
onColumnOrderChange,
@@ -176,7 +175,6 @@ function TanStackTableInner<TData>(
storageKey: columnStorageKey,
columns,
isGrouped,
respectColumnOrder,
});
// Use store values when columnStorageKey is provided, otherwise fall back to props/defaults
@@ -208,7 +206,6 @@ function TanStackTableInner<TData>(
handleRemoveColumn,
} = useColumnHandlers({
columnStorageKey,
respectColumnOrder,
effectiveSizing,
storeSetSizing,
storeSetOrder,

View File

@@ -24,8 +24,6 @@ jest.mock('../TanStackTable.module.scss', () => ({
}));
beforeAll(() => {
// jsdom doesn't include ResizeObserver — must direct-assign rather than
// spyOn (spyOn requires the property to already exist).
window.ResizeObserver = jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
@@ -869,110 +867,4 @@ describe('TanStackTableView Integration', () => {
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
});
});
describe('hasSingleColumn — gates the Remove popover per-column', () => {
const hasSingleColumnFlagPresent = (): boolean =>
Boolean(document.querySelector('th[data-single-column="true"]'));
it('is true when only one non-pinned column exists', async () => {
renderTanStackTable({
props: {
data: [{ id: '1', name: 'Item 1', value: 100 }],
columns: [
{
id: 'name',
header: 'Name',
accessorKey: 'name',
cell: ({ value }): string => String(value),
},
],
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
expect(hasSingleColumnFlagPresent()).toBe(true);
});
it('is false when multiple non-pinned columns exist (all removable)', async () => {
renderTanStackTable({});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
// 3 default columns (id/name/value), none pinned, none non-removable
// → table is not single-column.
expect(hasSingleColumnFlagPresent()).toBe(false);
});
it('is false when removable + non-removable mix exists (the body/timestamp case)', async () => {
renderTanStackTable({
props: {
data: [{ id: '1', name: 'Item 1', value: 100 }],
columns: [
{
id: 'name',
header: 'Timestamp',
accessorKey: 'name',
enableRemove: false,
cell: ({ value }): string => String(value),
},
{
id: 'value',
header: 'Body',
accessorKey: 'value',
enableRemove: false,
cell: ({ value }): string => String(value),
},
{
id: 'id',
header: 'User',
accessorKey: 'id',
enableRemove: true,
cell: ({ value }): string => String(value),
},
],
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
expect(hasSingleColumnFlagPresent()).toBe(false);
});
it('does not count pinned columns toward the total', async () => {
renderTanStackTable({
props: {
data: [{ id: '1', name: 'Item 1', value: 100 }],
columns: [
{
id: 'stateIndicator',
header: '',
pin: 'left',
accessorKey: 'id',
cell: ({ value }): string => String(value),
},
{
id: 'name',
header: 'Name',
accessorKey: 'name',
cell: ({ value }): string => String(value),
},
],
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
// 1 pinned + 1 non-pinned → only the non-pinned counts → single-column.
expect(hasSingleColumnFlagPresent()).toBe(true);
});
});
});

View File

@@ -168,53 +168,6 @@ describe('useColumnState', () => {
'a',
]);
});
it('ignores stored columnOrder when respectColumnOrder=false', () => {
const columns = [col('a'), col('b'), col('c')];
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, columns);
useColumnStore.getState().setColumnOrder(TEST_KEY, ['c', 'a', 'b']);
});
const { result } = renderHook(() =>
useColumnState({
storageKey: TEST_KEY,
columns,
respectColumnOrder: false,
}),
);
// Falls through to natural columns-array order; stored order is ignored.
expect(result.current.sortedColumns.map((c) => c.id)).toStrictEqual([
'a',
'b',
'c',
]);
});
it('honors stored columnOrder when respectColumnOrder=true (default)', () => {
const columns = [col('a'), col('b'), col('c')];
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, columns);
useColumnStore.getState().setColumnOrder(TEST_KEY, ['c', 'a', 'b']);
});
const { result } = renderHook(() =>
useColumnState({
storageKey: TEST_KEY,
columns,
respectColumnOrder: true,
}),
);
expect(result.current.sortedColumns.map((c) => c.id)).toStrictEqual([
'c',
'a',
'b',
]);
});
});
describe('actions', () => {

View File

@@ -152,8 +152,6 @@ export type TanStackTableProps<TData> = {
columns: TableColumnDef<TData>[];
/** Storage key for column state persistence (visibility, sizing, ordering). When set, enables unified column management. */
columnStorageKey?: string;
/** When false, the table renders columns in their natural array order and the persisted columnOrder slot is ignored on read and skipped on write. Use when an external source (e.g. preferences.selectColumns) is the canonical order. Default true. */
respectColumnOrder?: boolean;
columnSizing?: ColumnSizingState;
onColumnSizingChange?: Dispatch<SetStateAction<ColumnSizingState>>;
onColumnOrderChange?: (cols: TableColumnDef<TData>[]) => void;

View File

@@ -7,8 +7,6 @@ import { TableColumnDef } from './types';
export interface UseColumnHandlersOptions<TData> {
/** Storage key for persisting column state (enables store mode) */
columnStorageKey?: string;
/** When false, drag-reorder skips the persisted columnOrder write — order flows back via onColumnOrderChange only. */
respectColumnOrder?: boolean;
effectiveSizing: ColumnSizingState;
storeSetSizing: (sizing: ColumnSizingState) => void;
storeSetOrder: (columns: TableColumnDef<TData>[]) => void;
@@ -30,7 +28,6 @@ export interface UseColumnHandlersResult<TData> {
*/
export function useColumnHandlers<TData>({
columnStorageKey,
respectColumnOrder = true,
effectiveSizing,
storeSetSizing,
storeSetOrder,
@@ -53,12 +50,12 @@ export function useColumnHandlers<TData>({
const handleColumnOrderChange = useCallback(
(cols: TableColumnDef<TData>[]) => {
if (columnStorageKey && respectColumnOrder) {
if (columnStorageKey) {
storeSetOrder(cols);
}
onColumnOrderChange?.(cols);
},
[columnStorageKey, respectColumnOrder, storeSetOrder, onColumnOrderChange],
[columnStorageKey, storeSetOrder, onColumnOrderChange],
);
const handleRemoveColumn = useCallback(

View File

@@ -20,7 +20,6 @@ type UseColumnStateOptions<TData> = {
storageKey?: string;
columns: TableColumnDef<TData>[];
isGrouped?: boolean;
respectColumnOrder?: boolean;
};
type UseColumnStateResult<TData> = {
@@ -41,7 +40,6 @@ export function useColumnState<TData>({
storageKey,
columns,
isGrouped = false,
respectColumnOrder = true,
}: UseColumnStateOptions<TData>): UseColumnStateResult<TData> {
useEffect(() => {
if (storageKey) {
@@ -132,7 +130,7 @@ export function useColumnState<TData>({
}, [hiddenColumnIds, columns, isGrouped]);
const sortedColumns = useMemo((): TableColumnDef<TData>[] => {
if (!respectColumnOrder || columnOrder.length === 0) {
if (columnOrder.length === 0) {
return columns;
}
@@ -146,7 +144,7 @@ export function useColumnState<TData>({
});
return [...pinned, ...sortedRest];
}, [columns, columnOrder, respectColumnOrder]);
}, [columns, columnOrder]);
const hideColumn = useCallback(
(columnId: string) => {

View File

@@ -139,6 +139,7 @@ jest.mock('react-query', (): unknown => {
});
// mock other side-effecty modules
jest.mock('api/common/logEvent', () => jest.fn());
jest.mock('api/browser/localstorage/set', () => jest.fn());
jest.mock('utils/error', () => ({ showErrorNotification: jest.fn() }));

View File

@@ -33,8 +33,7 @@ export const REACT_QUERY_KEY = {
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
GET_TRACE_V2_WATERFALL: 'GET_TRACE_V2_WATERFALL',
GET_TRACE_V4_WATERFALL: 'GET_TRACE_V4_WATERFALL',
GET_TRACE_AGGREGATIONS: 'GET_TRACE_AGGREGATIONS',
GET_TRACE_V3_WATERFALL: 'GET_TRACE_V3_WATERFALL',
GET_TRACE_V2_FLAMEGRAPH: 'GET_TRACE_V2_FLAMEGRAPH',
GET_POD_LIST: 'GET_POD_LIST',
GET_NODE_LIST: 'GET_NODE_LIST',

View File

@@ -1,5 +0,0 @@
.alertHistory {
display: flex;
flex-direction: column;
gap: var(--spacing-10);
}

View File

@@ -1,5 +1,5 @@
.container {
.alert-history {
display: flex;
flex-direction: column;
height: 100%;
gap: 24px;
}

View File

@@ -3,13 +3,13 @@ import { useState } from 'react';
import Statistics from './Statistics/Statistics';
import Timeline from './Timeline/Timeline';
import styles from './AlertHistory.module.scss';
import './AlertHistory.styles.scss';
function AlertHistory(): JSX.Element {
const [totalCurrentTriggers, setTotalCurrentTriggers] = useState(0);
return (
<div className={styles.alertHistory}>
<div className="alert-history">
<Statistics
totalCurrentTriggers={totalCurrentTriggers}
setTotalCurrentTriggers={setTotalCurrentTriggers}

View File

@@ -1,40 +0,0 @@
.alertPopoverTriggerAction {
cursor: pointer;
}
.alertHistoryPopover {
:global(.ant-popover-inner) {
border: 1px solid var(--l1-border);
background: var(--l1-background) !important;
padding: 0 !important;
}
:global(.ant-popover-arrow) {
&::before {
background: var(--l1-background);
}
}
}
.contributorRowPopoverButtons {
display: flex;
flex-direction: column;
}
.contributorRowPopoverButtonsButton {
display: flex;
align-items: center;
gap: var(--spacing-3);
padding: var(--spacing-6) var(--spacing-7);
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
letter-spacing: 0.14px;
width: 160px;
cursor: pointer;
background: var(--l1-background);
border-color: var(--l1-border);
&:hover {
background: var(--l2-background);
}
}

View File

@@ -0,0 +1,15 @@
.alert-popover-trigger-action {
cursor: pointer;
}
.alert-history-popover {
.ant-popover-inner {
border: 1px solid var(--l1-border);
background: var(--l1-background) !important;
}
.ant-popover-arrow {
&::before {
background: var(--l1-background);
}
}
}

View File

@@ -7,7 +7,7 @@ import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { DraftingCompass } from '@signozhq/icons';
import styles from './AlertPopover.module.scss';
import './AlertPopover.styles.scss';
type Props = {
children: React.ReactNode;
@@ -24,30 +24,30 @@ function PopoverContent({
}): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<div className={styles.contributorRowPopoverButtons}>
<div className="contributor-row-popover-buttons">
{!!relatedLogsLink && (
<Link
to={`${ROUTES.LOGS_EXPLORER}?${relatedLogsLink}`}
className={styles.contributorRowPopoverButtonsButton}
className="contributor-row-popover-buttons__button"
>
<div>
<div className="icon">
<LogsIcon />
</div>
<div>View Logs</div>
<div className="text">View Logs</div>
</Link>
)}
{!!relatedTracesLink && (
<Link
to={`${ROUTES.TRACES_EXPLORER}?${relatedTracesLink}`}
className={styles.contributorRowPopoverButtonsButton}
className="contributor-row-popover-buttons__button"
>
<div>
<div className="icon">
<DraftingCompass
size={14}
color={isDarkMode ? Color.BG_VANILLA_400 : Color.TEXT_INK_400}
/>
</div>
<div>View Traces</div>
<div className="text">View Traces</div>
</Link>
)}
</div>
@@ -64,13 +64,13 @@ function AlertPopover({
relatedLogsLink,
}: Props): JSX.Element {
return (
<div className={styles.alertPopoverTriggerAction}>
<div className="alert-popover-trigger-action">
<Popover
showArrow={false}
placement="bottom"
color="linear-gradient(139deg, rgba(18, 19, 23, 1) 0%, rgba(18, 19, 23, 1) 98.68%)"
destroyTooltipOnHide
rootClassName={styles.alertHistoryPopover}
rootClassName="alert-history-popover"
content={
<PopoverContent
relatedTracesLink={relatedTracesLink}
@@ -112,3 +112,4 @@ export function ConditionalAlertPopover({
}
return <div>{children}</div>;
}
export default AlertPopover;

View File

@@ -4,5 +4,5 @@
height: 280px;
border: 1px solid var(--l1-border);
border-radius: 4px;
margin: 0 var(--spacing-8);
margin: 0 16px;
}

View File

@@ -3,7 +3,7 @@ import { AlertRuleStats } from 'types/api/alerts/def';
import StatsCardsRenderer from './StatsCardsRenderer/StatsCardsRenderer';
import TopContributorsRenderer from './TopContributorsRenderer/TopContributorsRenderer';
import styles from './Statistics.module.scss';
import './Statistics.styles.scss';
function Statistics({
setTotalCurrentTriggers,
@@ -13,7 +13,7 @@ function Statistics({
totalCurrentTriggers: AlertRuleStats['totalCurrentTriggers'];
}): JSX.Element {
return (
<div className={styles.statistics}>
<div className="statistics">
<StatsCardsRenderer setTotalCurrentTriggers={setTotalCurrentTriggers} />
<TopContributorsRenderer totalCurrentTriggers={totalCurrentTriggers} />
</div>

View File

@@ -1,102 +0,0 @@
.statsCard {
width: 21.7%;
border-right: 1px solid var(--l1-border);
padding: var(--spacing-4) var(--spacing-6) var(--spacing-6);
}
.statsCardEmpty {
justify-content: normal;
}
.statsCardTitleWrapper {
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
text-transform: uppercase;
font-size: var(--periscope-font-size-base);
line-height: 22px;
color: var(--l2-foreground);
font-weight: var(--font-weight-medium);
}
.durationIndicator {
display: flex;
align-items: center;
gap: var(--spacing-2);
}
.icon {
display: flex;
align-self: center;
}
.text {
text-transform: uppercase;
color: var(--l3-foreground);
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-semibold);
letter-spacing: 0.48px;
}
.statsCardStats {
margin-top: var(--spacing-10);
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
.countLabel {
color: var(--l1-foreground);
font-family: var(--periscope-font-family-mono);
font-size: var(--font-size-2xl);
line-height: 36px;
}
.statsCardAlertHistoryGraph {
margin-top: var(--spacing-16);
}
.alertHistoryGraph {
width: 100%;
height: 72px;
}
.changePercentage {
width: max-content;
display: flex;
padding: var(--spacing-2) var(--spacing-4);
border-radius: 20px;
align-items: center;
gap: var(--spacing-2);
}
// TODO(@signozhq/design-tokens): replace --text-forest-* with --success-foreground after release
.changePercentageSuccess {
background: color-mix(in srgb, var(--text-forest-500) 10%, transparent);
color: var(--text-forest-400);
}
.changePercentageError {
background: color-mix(in srgb, var(--danger-background) 10%, transparent);
color: var(--danger-foreground);
}
.changePercentageNoPreviousData {
color: var(--primary-foreground);
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
padding: var(--spacing-2) var(--spacing-8);
}
.changePercentageIcon {
display: flex;
align-self: center;
}
.changePercentageLabel {
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-medium);
line-height: 16px;
}

View File

@@ -0,0 +1,94 @@
.stats-card {
width: 21.7%;
border-right: 1px solid var(--l1-border);
padding: 9px 12px 13px;
&--empty {
justify-content: normal;
}
&__title-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
.title {
text-transform: uppercase;
font-size: 13px;
line-height: 22px;
color: var(--l2-foreground);
font-weight: 500;
}
.duration-indicator {
display: flex;
align-items: center;
gap: 4px;
.icon {
display: flex;
align-self: center;
}
.text {
text-transform: uppercase;
color: var(--l3-foreground);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.48px;
}
}
}
&__stats {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 4px;
.count-label {
color: var(--l1-foreground);
font-family: 'Geist Mono';
font-size: 24px;
line-height: 36px;
}
}
&__alert-history-graph {
margin-top: 80px;
.alert-history-graph {
width: 100%;
height: 72px;
}
}
}
.change-percentage {
width: max-content;
display: flex;
padding: 4px 8px;
border-radius: 20px;
align-items: center;
gap: 4px;
&--success {
background: color-mix(in srgb, var(--success-background) 10%, transparent);
color: var(--success-foreground);
}
&--error {
background: color-mix(in srgb, var(--error-background) 10%, transparent);
color: var(--error-foreground);
}
&--no-previous-data {
color: var(--primary-foreground);
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
padding: 4px 16px;
}
&__icon {
display: flex;
align-self: center;
}
&__label {
font-size: 12px;
font-weight: 500;
line-height: 16px;
}
}

View File

@@ -1,4 +1,3 @@
import cx from 'classnames';
import { Color } from '@signozhq/design-tokens';
import { Tooltip } from 'antd';
import { QueryParams } from 'constants/query';
@@ -13,7 +12,7 @@ import {
extractDayFromTimestamp,
} from './utils';
import styles from './StatsCard.module.scss';
import './StatsCard.styles.scss';
type ChangePercentageProps = {
percentage: number;
@@ -27,11 +26,11 @@ function ChangePercentage({
}: ChangePercentageProps): JSX.Element {
if (direction > 0) {
return (
<div className={cx(styles.changePercentage, styles.changePercentageSuccess)}>
<div className={styles.changePercentageIcon}>
<div className="change-percentage change-percentage--success">
<div className="change-percentage__icon">
<ArrowDownLeft size={14} color={Color.BG_FOREST_500} />
</div>
<div className={styles.changePercentageLabel}>
<div className="change-percentage__label">
{percentage}% vs Last {duration}
</div>
</div>
@@ -39,11 +38,11 @@ function ChangePercentage({
}
if (direction < 0) {
return (
<div className={cx(styles.changePercentage, styles.changePercentageError)}>
<div className={styles.changePercentageIcon}>
<div className="change-percentage change-percentage--error">
<div className="change-percentage__icon">
<ArrowUpRight size={14} color={Color.BG_CHERRY_500} />
</div>
<div className={styles.changePercentageLabel}>
<div className="change-percentage__label">
{percentage}% vs Last {duration}
</div>
</div>
@@ -51,13 +50,8 @@ function ChangePercentage({
}
return (
<div
className={cx(
styles.changePercentage,
styles.changePercentageNoPreviousData,
)}
>
<div className={styles.changePercentageLabel}>no previous data</div>
<div className="change-percentage change-percentage--no-previous-data">
<div className="change-percentage__label">no previous data</div>
</div>
);
}
@@ -109,27 +103,27 @@ function StatsCard({
const formattedEndTimeForTooltip = convertTimestampToLocaleDateString(endTime);
return (
<div className={cx(styles.statsCard, { [styles.statsCardEmpty]: isEmpty })}>
<div className={styles.statsCardTitleWrapper}>
<div className={styles.title}>{title}</div>
<div className={styles.durationIndicator}>
<div className={styles.icon}>
<div className={`stats-card ${isEmpty ? 'stats-card--empty' : ''}`}>
<div className="stats-card__title-wrapper">
<div className="title">{title}</div>
<div className="duration-indicator">
<div className="icon">
<Calendar size={14} color={Color.BG_SLATE_200} />
</div>
{relativeTime ? (
<div className={styles.text}>{displayTime}</div>
<div className="text">{displayTime}</div>
) : (
<Tooltip
title={`From ${formattedStartTimeForTooltip} to ${formattedEndTimeForTooltip}`}
>
<div className={styles.text}>{displayTime}</div>
<div className="text">{displayTime}</div>
</Tooltip>
)}
</div>
</div>
<div className={styles.statsCardStats}>
<div className={styles.countLabel}>
<div className="stats-card__stats">
<div className="count-label">
{isEmpty ? emptyMessage : displayValue || totalCurrentCount}
</div>
@@ -140,8 +134,8 @@ function StatsCard({
/>
</div>
<div className={styles.statsCardAlertHistoryGraph}>
<div className={styles.alertHistoryGraph}>
<div className="stats-card__alert-history-graph">
<div className="alert-history-graph">
{!isEmpty && timeSeries.length > 1 && (
<StatsGraph timeSeries={timeSeries} changeDirection={changeDirection} />
)}

View File

@@ -1,45 +0,0 @@
.topContributorsCard {
width: 56.6%;
overflow: hidden;
}
.topContributorsCardHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-4) var(--spacing-6);
border-bottom: 1px solid var(--l1-border);
}
.title {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-medium);
line-height: 22px;
letter-spacing: 0.52px;
text-transform: uppercase;
}
.viewAll {
display: flex;
align-items: center;
gap: var(--spacing-2);
cursor: pointer;
padding: 0;
height: var(--line-height-20);
&:hover {
background-color: transparent !important;
}
}
.label {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
}
.icon {
display: flex;
}

View File

@@ -0,0 +1,163 @@
.top-contributors-card {
width: 56.6%;
overflow: hidden;
&--view-all {
width: auto;
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid var(--l1-border);
.title {
color: var(--l2-foreground);
font-size: 13px;
font-weight: 500;
line-height: 22px;
letter-spacing: 0.52px;
text-transform: uppercase;
}
.view-all {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
padding: 0;
height: 20px;
&:hover {
background-color: transparent !important;
}
.label {
color: var(--l2-foreground);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}
.icon {
display: flex;
}
}
}
.contributors-row {
height: 80px;
}
.top-contributors-progress {
--progress-background: transparent;
}
&__content {
.ant-table {
&-cell {
padding: 12px !important;
}
}
.contributors-row {
background: var(--l1-background);
td {
border: none !important;
}
&:not(:last-of-type) td {
border-bottom: 1px solid var(--l1-border) !important;
}
}
.total-contribution {
color: var(--primary-foreground);
font-family: 'Geist Mono';
font-size: 12px;
font-weight: 500;
letter-spacing: -0.06px;
padding: 4px 8px;
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
border-radius: 50px;
width: max-content;
}
}
.empty-content {
margin: 16px 12px;
padding: 40px 45px;
display: flex;
flex-direction: column;
gap: 12px;
border: 1px dashed var(--l1-border);
border-radius: 6px;
&__icon {
font-family: Inter;
font-size: 20px;
line-height: 26px;
letter-spacing: -0.103px;
}
&__text {
color: var(--l2-foreground);
line-height: 18px;
.bold-text {
color: var(--l1-foreground);
font-weight: 500;
}
}
&__button-wrapper {
margin-top: 12px;
.configure-alert-rule-button {
padding: 8px 16px;
border-radius: 2px;
background: var(--l3-background);
border-width: 0;
color: var(--l1-foreground);
line-height: 24px;
font-size: 12px;
font-weight: 500;
display: flex;
align-items: center;
}
}
}
}
.ant-popover-inner:has(.contributor-row-popover-buttons) {
padding: 0 !important;
}
.contributor-row-popover-buttons {
display: flex;
flex-direction: column;
&__button {
display: flex;
align-items: center;
gap: 6px;
padding: 12px 15px;
color: var(--l2-foreground);
font-size: 14px;
letter-spacing: 0.14px;
width: 160px;
cursor: pointer;
background: var(--l1-background);
border-color: var(--l1-border);
.text,
.icon {
color: var(--l1-foreground);
}
&:hover {
background: var(--l2-background);
.text,
.icon {
color: var(--l1-foreground);
}
}
.icon {
display: flex;
}
}
}
.view-all-drawer {
border-radius: 4px;
}

View File

@@ -10,7 +10,7 @@ import TopContributorsContent from './TopContributorsContent';
import { TopContributorsCardProps } from './types';
import ViewAllDrawer from './ViewAllDrawer';
import styles from './TopContributorsCard.module.scss';
import './TopContributorsCard.styles.scss';
function TopContributorsCard({
topContributorsData,
@@ -48,17 +48,13 @@ function TopContributorsCard({
return (
<>
<div className={styles.topContributorsCard}>
<div className={styles.topContributorsCardHeader}>
<div className={styles.title}>top contributors</div>
<div className="top-contributors-card">
<div className="top-contributors-card__header">
<div className="title">top contributors</div>
{topContributorsData.length > 3 && (
<Button
type="text"
className={styles.viewAll}
onClick={toggleViewAllDrawer}
>
<div className={styles.label}>View all</div>
<div className={styles.icon}>
<Button type="text" className="view-all" onClick={toggleViewAllDrawer}>
<div className="label">View all</div>
<div className="icon">
<ArrowRight
size={14}
color={isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}

View File

@@ -1,27 +0,0 @@
.emptyContent {
margin: var(--spacing-8) var(--spacing-6);
padding: var(--spacing-20) 45px;
display: flex;
flex-direction: column;
gap: var(--spacing-6);
border: 1px dashed var(--l1-border);
border-radius: 6px;
}
.emptyContentIcon {
font-family: var(--font-family-inter);
font-size: var(--font-size-xl);
line-height: 26px;
letter-spacing: -0.103px;
}
.emptyContentText {
color: var(--l2-foreground);
line-height: var(--line-height-18);
}
.topContributorsCardContent {
:global(.ant-table-cell) {
padding: var(--spacing-6) !important;
}
}

View File

@@ -1,4 +1,3 @@
import styles from './TopContributorsContent.module.scss';
import TopContributorsRows from './TopContributorsRows';
import { TopContributorsCardProps } from './types';
@@ -10,9 +9,9 @@ function TopContributorsContent({
if (isEmpty) {
return (
<div className={styles.emptyContent}>
<div className={styles.emptyContentIcon}></div>
<div className={styles.emptyContentText}>
<div className="empty-content">
<div className="empty-content__icon"></div>
<div className="empty-content__text">
Top contributors highlight the most frequently triggering group-by
attributes in multi-dimensional alerts
</div>
@@ -21,7 +20,7 @@ function TopContributorsContent({
}
return (
<div className={styles.topContributorsCardContent}>
<div className="top-contributors-card__content">
<TopContributorsRows
topContributors={topContributorsData.slice(0, 3)}
totalCurrentTriggers={totalCurrentTriggers}

View File

@@ -1,28 +0,0 @@
.contributorsRow {
height: 80px;
background: var(--l1-background);
td {
border: none !important;
}
&:not(:last-of-type) td {
border-bottom: 1px solid var(--l1-border) !important;
}
}
.topContributorsProgress {
--progress-background: transparent;
}
.totalContribution {
color: var(--primary-foreground);
font-family: var(--periscope-font-family-mono);
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-medium);
letter-spacing: -0.06px;
padding: var(--spacing-2) var(--spacing-4);
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
border-radius: 50px;
width: max-content;
}

View File

@@ -12,8 +12,6 @@ import {
AlertRuleTopContributors,
} from 'types/api/alerts/def';
import styles from './TopContributorsRows.module.scss';
function TopContributorsRows({
topContributors,
totalCurrentTriggers,
@@ -55,7 +53,7 @@ function TopContributorsRows({
percent={(count / totalCurrentTriggers) * 100}
showInfo={false}
strokeColor={Color.BG_ROBIN_500}
className={styles.topContributorsProgress}
className="top-contributors-progress"
/>
</ConditionalAlertPopover>
),
@@ -70,7 +68,7 @@ function TopContributorsRows({
relatedTracesLink={record.relatedTracesLink}
relatedLogsLink={record.relatedLogsLink}
>
<div className={styles.totalContribution}>
<div className="total-contribution">
{count}/{totalCurrentTriggers}
</div>
</ConditionalAlertPopover>
@@ -90,7 +88,7 @@ function TopContributorsRows({
return (
<Table
rowClassName={styles.contributorsRow}
rowClassName="contributors-row"
rowKey={(row): string => `top-contributor-${row.fingerprint}`}
onRow={handleRowClick}
columns={columns}

View File

@@ -1,13 +0,0 @@
.topContributorsCardViewAll {
width: auto;
}
.topContributorsCardContent {
:global(.ant-table-cell) {
padding: var(--spacing-6) !important;
}
}
.viewAllDrawer {
border-radius: 4px;
}

View File

@@ -3,7 +3,6 @@ import { Drawer } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { AlertRuleStats, AlertRuleTopContributors } from 'types/api/alerts/def';
import styles from './ViewAllDrawer.module.scss';
import TopContributorsRows from './TopContributorsRows';
function ViewAllDrawer({
@@ -25,15 +24,15 @@ function ViewAllDrawer({
onClose={toggleViewAllDrawer}
placement="right"
width="50%"
className={styles.viewAllDrawer}
className="view-all-drawer"
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
title="Viewing All Contributors"
>
<div className={styles.topContributorsCardViewAll}>
<div className={styles.topContributorsCardContent}>
<div className="top-contributors-card--view-all">
<div className="top-contributors-card__content">
<TopContributorsRows
topContributors={topContributorsData}
totalCurrentTriggers={totalCurrentTriggers}

View File

@@ -0,0 +1,35 @@
.timeline-graph {
display: flex;
flex-direction: column;
gap: 24px;
background: var(--l2-background);
padding: 12px;
border-radius: 4px;
border: 1px solid var(--l1-border);
height: 150px;
&__title {
width: max-content;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
color: var(--l1-foreground);
font-size: 12px;
line-height: 18px;
letter-spacing: -0.06px;
}
&__chart {
.chart-placeholder {
width: 100%;
height: 52px;
background: color-mix(in srgb, var(--l1-foreground) 12%, transparent);
display: flex;
align-items: center;
justify-content: center;
.chart-icon {
font-size: 2rem;
}
}
}
}

View File

@@ -1,22 +0,0 @@
.timelineGraph {
display: flex;
flex-direction: column;
gap: var(--spacing-10);
background: var(--l2-background);
padding: var(--spacing-6);
border-radius: 4px;
border: 1px solid var(--l1-border);
height: 150px;
}
.timelineGraphTitle {
width: max-content;
padding: var(--spacing-1) var(--spacing-4);
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
color: var(--l1-foreground);
font-size: var(--periscope-font-size-small);
line-height: var(--line-height-18);
letter-spacing: -0.06px;
}

View File

@@ -4,7 +4,7 @@ import DataStateRenderer from 'periscope/components/DataStateRenderer/DataStateR
import Graph from '../Graph/Graph';
import styles from './GraphWrapper.module.scss';
import '../Graph/Graph.styles.scss';
function GraphWrapper({
totalCurrentTriggers,
@@ -40,11 +40,11 @@ function GraphWrapper({
// }, [startTime]);
return (
<div className={styles.timelineGraph}>
<div className={styles.timelineGraphTitle}>
<div className="timeline-graph">
<div className="timeline-graph__title">
{totalCurrentTriggers} triggers in {relativeTime}
</div>
<div>
<div className="timeline-graph__chart">
<DataStateRenderer
isLoading={isLoading}
isError={isError || !isValidRuleId || !ruleId}

View File

@@ -1,70 +0,0 @@
.timelineTable {
border-top: 1px solid var(--l1-border);
border-radius: 6px;
overflow: hidden;
margin-top: var(--spacing-2);
min-height: 600px;
:global(.ant-table) {
background: var(--l1-background);
}
:global(.ant-table-cell) {
padding: var(--spacing-6) var(--spacing-8) !important;
vertical-align: baseline;
&::before {
display: none;
}
}
:global(.ant-table-thead) > tr > th {
border-color: var(--l1-border);
background: var(--l1-background);
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-medium);
padding: var(--spacing-6) var(--spacing-8) var(--spacing-4) !important;
}
:global(.ant-table-tbody) > tr > td {
border: none;
}
:global(.ant-table.ant-table-middle) {
border-bottom: 1px solid var(--l1-border);
border-left: 1px solid var(--l1-border);
border-right: 1px solid var(--l1-border);
border-radius: 6px;
}
:global(.ant-pagination-item-active) {
display: flex;
width: var(--spacing-10);
height: var(--spacing-10);
align-items: center;
justify-content: center;
padding: var(--spacing-0) var(--spacing-4);
border-radius: 2px;
background: var(--primary-background);
& > a {
color: var(--primary-foreground);
line-height: var(--line-height-20);
font-weight: var(--font-weight-medium);
}
}
}
.alertRuleCreatedAt {
font-size: var(--periscope-font-size-base);
color: var(--l2-foreground);
line-height: var(--line-height-18);
letter-spacing: -0.07px;
}
.alertHistoryLabelSearch {
:global(.ant-select-selector) {
border: none;
background: var(--l2-background);
}
}

View File

@@ -0,0 +1,89 @@
.timeline-table {
border-top: 1px solid var(--l1-border);
border-radius: 6px;
overflow: hidden;
margin-top: 4px;
min-height: 600px;
.ant-table {
background: var(--l1-background);
&-cell {
padding: 12px 16px !important;
vertical-align: baseline;
&::before {
display: none;
}
}
&-thead > tr > th {
border-color: var(--l1-border);
background: var(--l1-background);
font-size: 12px;
font-weight: 500;
padding: 12px 16px 8px !important;
}
&-tbody > tr > td {
border: none;
}
}
.label-filter {
padding: 6px 8px;
border-radius: 4px;
background: var(--l1-foreground);
border-width: 0;
line-height: 18px;
& ::placeholder {
color: var(--l2-foreground);
font-size: 12px;
letter-spacing: 0.6px;
text-transform: uppercase;
font-weight: 500;
}
}
.alert-rule {
&-value,
&__created-at {
font-size: 14px;
color: var(--l2-foreground);
}
&-value {
font-weight: 500;
line-height: 20px;
}
&__created-at {
line-height: 18px;
letter-spacing: -0.07px;
}
}
.ant-table.ant-table-middle {
border-bottom: 1px solid var(--l1-border);
border-left: 1px solid var(--l1-border);
border-right: 1px solid var(--l1-border);
border-radius: 6px;
}
.ant-pagination-item {
&-active {
display: flex;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
padding: 1px 8px;
border-radius: 2px;
background: var(--primary-background);
& > a {
color: var(--primary-foreground);
line-height: 20px;
font-weight: 500;
}
}
}
.alert-history-label-search {
.ant-select-selector {
border: none;
background: var(--l2-background);
}
}
}

View File

@@ -13,7 +13,7 @@ import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { timelineTableColumns } from './useTimelineTable';
import styles from './Table.module.scss';
import './Table.styles.scss';
function TimelineTable(): JSX.Element {
const [filters, setFilters] = useState<TagFilter>(initialFilters);
@@ -54,7 +54,7 @@ function TimelineTable(): JSX.Element {
});
return (
<div className={styles.timelineTable}>
<div className="timeline-table">
<Table
rowKey={(row): string => `${row.fingerprint}-${row.value}-${row.unixMilli}`}
columns={timelineTableColumns({

View File

@@ -17,8 +17,6 @@ import AlertState from 'pages/AlertDetails/AlertHeader/AlertState/AlertState';
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import styles from './Table.module.scss';
const transformLabelsToQbKeys = (
labels: AlertRuleTimelineTableResponse['labels'],
): AttributeKey[] => Object.keys(labels).flatMap((key) => [{ key }]);
@@ -58,7 +56,7 @@ function LabelFilter({
<ClientSideQBSearch
onChange={handleSearch}
filters={filters}
className={styles.alertHistoryLabelSearch}
className="alert-history-label-search"
attributeKeys={transformedKeys}
attributeValuesMap={attributesMap}
suffixIcon={
@@ -90,21 +88,29 @@ export const timelineTableColumns = ({
dataIndex: 'state',
sorter: true,
width: 140,
render: (value): JSX.Element => <AlertState state={value} showLabel />,
render: (value): JSX.Element => (
<div className="alert-rule-state">
<AlertState state={value} showLabel />
</div>
),
},
{
title: (
<LabelFilter setFilters={setFilters} filters={filters} labels={labels} />
),
dataIndex: 'labels',
render: (labels): JSX.Element => <AlertLabels labels={labels} />,
render: (labels): JSX.Element => (
<div className="alert-rule-labels">
<AlertLabels labels={labels} />
</div>
),
},
{
title: 'CREATED AT',
dataIndex: 'unixMilli',
width: 200,
render: (value): JSX.Element => (
<div className={styles.alertRuleCreatedAt}>
<div className="alert-rule__created-at">
{formatTimezoneAdjustedTimestamp(value, DATE_TIME_FORMATS.DASH_DATETIME)}
</div>
),
@@ -119,7 +125,7 @@ export const timelineTableColumns = ({
relatedLogsLink={record.relatedLogsLink}
>
<Button type="text" ghost>
<Ellipsis size="md" />
<Ellipsis className="dropdown-icon" size="md" />
</Button>
</ConditionalAlertPopover>
),

View File

@@ -1,35 +0,0 @@
.timelineTabsAndFilters {
display: flex;
justify-content: space-between;
align-items: center;
}
.resetButton,
.top5Contributors {
display: flex;
align-items: center;
gap: var(--spacing-5);
}
.comingSoon {
display: inline-flex;
padding: var(--spacing-2) var(--spacing-4);
border-radius: 20px;
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
justify-content: center;
align-items: center;
gap: var(--spacing-2);
}
.comingSoonText {
color: var(--text-sienna-400);
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-medium);
letter-spacing: -0.05px;
line-height: normal;
}
.comingSoonIcon {
display: flex;
}

View File

@@ -0,0 +1,32 @@
.timeline-tabs-and-filters {
display: flex;
justify-content: space-between;
align-items: center;
.reset-button,
.top-5-contributors {
display: flex;
align-items: center;
gap: 10px;
}
.coming-soon {
display: inline-flex;
padding: 4px 8px;
border-radius: 20px;
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
justify-content: center;
align-items: center;
gap: 5px;
&__text {
color: var(--text-sienna-400);
font-size: 10px;
font-weight: 500;
letter-spacing: -0.05px;
line-height: normal;
}
&__icon {
display: flex;
}
}
}

View File

@@ -6,13 +6,13 @@ import history from 'lib/history';
import { Info } from '@signozhq/icons';
import Tabs2 from 'periscope/components/Tabs2';
import styles from './TabsAndFilters.module.scss';
import './TabsAndFilters.styles.scss';
function ComingSoon(): JSX.Element {
return (
<div className={styles.comingSoon}>
<div className={styles.comingSoonText}>Coming Soon</div>
<div className={styles.comingSoonIcon}>
<div className="coming-soon">
<div className="coming-soon__text">Coming Soon</div>
<div className="coming-soon__icon">
<Info size={10} color={Color.BG_SIENNA_400} />
</div>
</div>
@@ -27,7 +27,7 @@ function TimelineTabs(): JSX.Element {
{
value: TimelineTab.TOP_5_CONTRIBUTORS,
label: (
<div className={styles.top5Contributors}>
<div className="top-5-contributors">
Top 5 Contributors
<ComingSoon />
</div>
@@ -80,7 +80,7 @@ function TimelineFilters(): JSX.Element {
function TabsAndFilters(): JSX.Element {
return (
<div className={styles.timelineTabsAndFilters}>
<div className="timeline-tabs-and-filters">
<TimelineTabs />
<TimelineFilters />
</div>

View File

@@ -1,14 +0,0 @@
.timeline {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
margin: 0 var(--spacing-8);
}
.timelineTitle {
color: var(--l1-foreground);
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
}

View File

@@ -0,0 +1,14 @@
.timeline {
display: flex;
flex-direction: column;
gap: 8px;
margin: 0 16px;
&__title {
color: var(--l1-foreground);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
}

View File

@@ -2,7 +2,7 @@ import GraphWrapper from './GraphWrapper/GraphWrapper';
import TimelineTable from './Table/Table';
import TabsAndFilters from './TabsAndFilters/TabsAndFilters';
import styles from './Timeline.module.scss';
import './Timeline.styles.scss';
function TimelineTableRenderer(): JSX.Element {
return <TimelineTable />;
@@ -14,15 +14,15 @@ function Timeline({
totalCurrentTriggers: number;
}): JSX.Element {
return (
<div className={styles.timeline}>
<div className={styles.timelineTitle}>Timeline</div>
<div>
<div className="timeline">
<div className="timeline__title">Timeline</div>
<div className="timeline__tabs-and-filters">
<TabsAndFilters />
</div>
<div>
<div className="timeline__graph">
<GraphWrapper totalCurrentTriggers={totalCurrentTriggers} />
</div>
<div>
<div className="timeline__table">
<TimelineTableRenderer />
</div>
</div>

View File

@@ -1,183 +0,0 @@
.anomalyAlertEvaluationView {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: var(--spacing-4);
width: 100%;
height: 100%;
:global(.uplot-tooltip) {
background-color: rgb(0 0 0 / 90%);
box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
color: #ddd;
font-size: var(--periscope-font-size-base);
line-height: 1.4;
padding: var(--spacing-4) var(--spacing-6);
pointer-events: none;
position: absolute;
z-index: 100;
max-height: 500px;
width: 280px;
overflow-y: auto;
display: none;
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136 136 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
:global(.uplot-tooltip-title) {
font-weight: var(--font-weight-semibold);
margin-bottom: var(--spacing-2);
}
:global(.uplot-tooltip-series) {
display: flex;
gap: var(--spacing-2);
padding: var(--spacing-2) 0;
align-items: center;
}
:global(.uplot-tooltip-series-name) {
margin-right: var(--spacing-2);
}
:global(.uplot-tooltip-band) {
font-style: italic;
color: #666;
}
:global(.uplot-tooltip-marker) {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: var(--spacing-4);
vertical-align: middle;
}
:global(.uplot) {
:global(.u-title) {
text-align: center;
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-normal);
display: flex;
height: 40px;
align-items: center;
}
:global(.u-legend) {
display: flex;
margin-top: var(--spacing-8);
tbody {
width: 100%;
:global(.u-series) {
display: inline-flex;
}
}
}
}
}
.chartSection {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.chartSectionMultiSeries {
composes: chartSection;
width: calc(100% - 240px);
}
.noDataContainer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: var(--spacing-4);
}
.seriesSelection {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
width: 240px;
padding: 0 var(--spacing-4);
height: 100%;
}
.seriesList {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
height: 100%;
}
.seriesListSearch {
margin-bottom: var(--spacing-8);
}
.seriesListTitle {
margin-top: var(--spacing-6);
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-normal);
}
.seriesListItems {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
height: 100%;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.1rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136 136 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
.seriesListItem {
display: flex;
flex-direction: row;
gap: var(--spacing-4);
cursor: pointer;
}
.seriesListItemColor {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-flex;
margin-right: var(--spacing-4);
vertical-align: middle;
}

View File

@@ -0,0 +1,180 @@
.anomaly-alert-evaluation-view {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 8px;
width: 100%;
height: 100%;
.anomaly-alert-evaluation-view-chart-section {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
&.has-multi-series-data {
width: calc(100% - 240px);
}
.anomaly-alert-evaluation-view-no-data-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8px;
}
}
.anomaly-alert-evaluation-view-series-selection {
display: flex;
flex-direction: column;
gap: 8px;
width: 240px;
padding: 0px 8px;
height: 100%;
.anomaly-alert-evaluation-view-series-list {
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
.anomaly-alert-evaluation-view-series-list-search {
margin-bottom: 16px;
}
.anomaly-alert-evaluation-view-series-list-title {
margin-top: 12px;
font-size: 13px !important;
font-weight: 400;
}
.anomaly-alert-evaluation-view-series-list-items {
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
overflow-y: auto;
.anomaly-alert-evaluation-view-series-list-item {
display: flex;
flex-direction: row;
gap: 8px;
.anomaly-alert-evaluation-view-series-list-item-color {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-flex;
margin-right: 8px;
vertical-align: middle;
}
cursor: pointer;
}
&::-webkit-scrollbar {
width: 0.1rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
}
}
.uplot {
.u-title {
text-align: center;
font-size: 18px;
font-weight: 400;
display: flex;
height: 40px;
font-size: 13px;
align-items: center;
}
.u-legend {
display: flex;
margin-top: 16px;
tbody {
width: 100%;
.u-series {
display: inline-flex;
}
}
}
}
}
.uplot-tooltip {
background-color: rgba(0, 0, 0, 0.9);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
color: #ddd;
font-size: 13px;
line-height: 1.4;
padding: 8px 12px;
pointer-events: none;
position: absolute;
z-index: 100;
max-height: 500px;
width: 280px;
overflow-y: auto;
display: none; /* Hide tooltip by default */
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
.uplot-tooltip-title {
font-weight: bold;
margin-bottom: 4px;
}
.uplot-tooltip-series {
display: flex;
gap: 4px;
padding: 4px 0px;
align-items: center;
}
.uplot-tooltip-series-name {
margin-right: 4px;
}
.uplot-tooltip-band {
font-style: italic;
color: #666;
}
.uplot-tooltip-marker {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 8px;
vertical-align: middle;
}

View File

@@ -15,7 +15,7 @@ import uPlot from 'uplot';
import tooltipPlugin from './tooltipPlugin';
import 'uplot/dist/uPlot.min.css';
import styles from './AnomalyAlertEvaluationView.module.scss';
import './AnomalyAlertEvaluationView.styles.scss';
const { Search } = Input;
@@ -284,11 +284,11 @@ function AnomalyAlertEvaluationView({
}, 300);
return (
<div className={styles.anomalyAlertEvaluationView}>
<div className="anomaly-alert-evaluation-view">
<div
className={
allSeries.length > 1 ? styles.chartSectionMultiSeries : styles.chartSection
}
className={`anomaly-alert-evaluation-view-chart-section ${
allSeries.length > 1 ? 'has-multi-series-data' : ''
}`}
ref={graphRef}
>
{allSeries.length > 0 ? (
@@ -298,7 +298,7 @@ function AnomalyAlertEvaluationView({
chartRef={chartRef}
/>
) : (
<div className={styles.noDataContainer}>
<div className="anomaly-alert-evaluation-view-no-data-container">
<ChartLine size={48} strokeWidth={0.5} />
<Typography>No Data</Typography>
@@ -307,20 +307,20 @@ function AnomalyAlertEvaluationView({
</div>
{allSeries.length > 1 && (
<div className={styles.seriesSelection}>
<div className="anomaly-alert-evaluation-view-series-selection">
{allSeries.length > 1 && (
<div className={styles.seriesList}>
<div className="anomaly-alert-evaluation-view-series-list">
<Search
className={styles.seriesListSearch}
className="anomaly-alert-evaluation-view-series-list-search"
placeholder="Search a series"
allowClear
onChange={handleSearchValueChange}
/>
<div className={styles.seriesListItems}>
<div className="anomaly-alert-evaluation-view-series-list-items">
{filteredSeriesKeys.length > 0 && (
<Checkbox
className={styles.seriesListItem}
className="anomaly-alert-evaluation-view-series-list-item"
name="series"
value={selectedSeries === null}
onChange={(): void => handleSeriesChange(null)}
@@ -332,14 +332,14 @@ function AnomalyAlertEvaluationView({
{filteredSeriesKeys.map((seriesKey) => (
<div key={seriesKey}>
<Checkbox
className={styles.seriesListItem}
className="anomaly-alert-evaluation-view-series-list-item"
key={seriesKey}
name="series"
value={selectedSeries === seriesKey}
onChange={(): void => handleSeriesChange(seriesKey)}
>
<div
className={styles.seriesListItemColor}
className="anomaly-alert-evaluation-view-series-list-item-color"
style={{ backgroundColor: seriesData[seriesKey].color }}
/>

View File

@@ -1,7 +1,7 @@
import { QueryClient, QueryClientProvider } from 'react-query';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { logEventMock } from '__tests__/logEventMock';
import logEvent from 'api/common/logEvent';
import { GlobalShortcuts } from 'constants/shortcuts/globalShortcuts';
import { USER_PREFERENCES } from 'constants/userPreferences';
import {
@@ -24,6 +24,8 @@ jest.mock('providers/cmdKProvider', () => ({
}),
}));
jest.mock('api/common/logEvent', () => jest.fn());
// Mock the AppContext
const mockUpdateUserPreferenceInContext = jest.fn();
@@ -137,7 +139,7 @@ describe('Sidebar Toggle Shortcut', () => {
it('should log the toggle event with correct parameters', async () => {
const user = userEvent.setup();
const mockHandleShortcut = jest.fn(() => {
logEventMock('Global Shortcut: Sidebar Toggle', {
logEvent('Global Shortcut: Sidebar Toggle', {
previousState: false,
newState: true,
});
@@ -153,13 +155,10 @@ describe('Sidebar Toggle Shortcut', () => {
await user.keyboard(SHIFT_B_KEYBOARD_SHORTCUT);
expect(logEventMock).toHaveBeenCalledWith(
'Global Shortcut: Sidebar Toggle',
{
previousState: false,
newState: true,
},
);
expect(logEvent).toHaveBeenCalledWith('Global Shortcut: Sidebar Toggle', {
previousState: false,
newState: true,
});
});
it('should update user preference in context', async () => {

View File

@@ -1,199 +0,0 @@
.billingContainer {
margin-bottom: var(--spacing-20);
padding-top: 36px;
width: 90%;
margin: 0 auto;
.pageHeader {
margin-bottom: var(--spacing-8);
.pageHeaderTitle {
font-weight: var(--label-medium-500-font-weight);
font-size: var(--label-medium-500-font-size);
line-height: 32px;
letter-spacing: -0.08px;
color: var(--l1-foreground);
}
.pageHeaderSubtitle {
font-size: var(--font-size-sm);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
color: var(--l2-foreground);
}
}
.pageInfoTitle {
margin: 0;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
color: var(--l1-foreground);
}
.pageInfoSubtitle {
margin: 0;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
line-height: var(--line-height-18);
letter-spacing: -0.07px;
color: var(--l2-foreground);
}
.pageInfo {
:global(.ant-card) {
padding: var(--padding-3);
}
.billingManageBtn {
background: var(--l3-background);
&:hover {
background: var(--l3-background-hover);
}
}
}
.billingSummary {
margin: var(--spacing-12) var(--spacing-4);
}
.billingDetails {
margin: var(--spacing-12) 0;
border: 1px solid var(--l1-border);
border-radius: 2px;
overflow: hidden;
:global {
.ant-table {
background: var(--l2-background);
}
.ant-table-thead > tr > th {
height: 52px;
padding: 0 var(--padding-4);
color: var(--l3-foreground);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
letter-spacing: 0.48px;
text-transform: uppercase;
}
.ant-table-tbody > tr > td {
height: 52px;
padding: 0 var(--padding-4);
background: var(--l2-background);
border-bottom: 1px solid var(--l1-border);
color: var(--l2-foreground);
font-size: var(--font-size-sm);
&:first-child {
color: var(--l1-foreground);
}
&:not(:first-child) {
font-feature-settings:
'zero' 1,
'lnum' 1,
'tnum' 1;
}
}
.ant-table-tbody > tr:last-child > td {
border-bottom: none;
}
.ant-table-tbody > tr:hover > td {
background: var(--l2-background) !important;
}
}
.billingDetailsHeaderCell {
position: relative;
background: var(--l2-background) !important;
border: none !important;
border-bottom: 1px solid var(--l1-border) !important;
box-shadow: none !important;
&::after {
content: '';
position: absolute;
inset-block: 0;
inset-inline-end: 0;
width: 2px;
background: var(--l2-background);
z-index: 1;
}
}
}
.upgradePlanBenefits {
margin: 0 var(--spacing-4);
border: 1px solid var(--l1-border);
border-radius: 5px;
padding: 0 var(--padding-12);
.planBenefits {
.planBenefit {
display: flex;
align-items: center;
gap: var(--spacing-8);
margin: var(--spacing-8) 0;
}
}
}
.billingGraphSection {
border: 1px solid var(--l1-border);
border-radius: 4px;
overflow: hidden;
margin-bottom: var(--spacing-4);
.billingGraphFooter {
display: flex;
gap: var(--spacing-4);
padding: var(--padding-3) var(--padding-4);
border-top: 1px solid var(--l1-border);
background: var(--l2-background);
.billingFooterBtn {
background: var(--l3-background);
&:hover {
background: var(--l3-background-hover);
}
}
}
}
.emptyGraphCard {
:global(.ant-card-body) {
height: 40vh;
display: flex;
justify-content: center;
align-items: center;
}
}
.billingUpdateNote {
margin-top: var(--spacing-8);
font-family: var(--font-family-inter);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 22px;
letter-spacing: -0.07px;
}
:global {
.ant-skeleton.ant-skeleton-element.ant-skeleton-active {
width: 100%;
min-width: 100%;
}
.ant-skeleton.ant-skeleton-element .ant-skeleton-input {
min-width: 100% !important;
}
}
}

View File

@@ -0,0 +1,70 @@
.billing-container {
margin-bottom: 40px;
padding-top: 36px;
width: 90%;
margin: 0 auto;
.billing-summary {
margin: 24px 8px;
}
.billing-details {
margin: 24px 0px;
.ant-table-title {
color: var(--l2-foreground);
background-color: var(--l3-background);
}
.ant-table-cell {
background-color: var(--l1-background);
border-color: var(--l1-border);
}
.ant-table-tbody {
td {
border-color: var(--l1-border);
}
}
}
.upgrade-plan-benefits {
margin: 0px 8px;
border: 1px solid var(--l1-border);
border-radius: 5px;
padding: 0 48px;
.plan-benefits {
.plan-benefit {
display: flex;
align-items: center;
gap: 16px;
margin: 16px 0;
}
}
}
.empty-graph-card {
.ant-card-body {
height: 40vh;
display: flex;
justify-content: center;
align-items: center;
}
}
.billing-update-note {
text-align: left;
font-size: 13px;
color: var(--l2-foreground);
margin-top: 16px;
}
}
.ant-skeleton.ant-skeleton-element.ant-skeleton-active {
width: 100%;
min-width: 100%;
}
.ant-skeleton.ant-skeleton-element .ant-skeleton-input {
min-width: 100% !important;
}

View File

@@ -38,7 +38,7 @@ describe('BillingContainer', () => {
});
expect(pricePerUnit).toBeInTheDocument();
const cost = await screen.findByRole('columnheader', {
name: /cost/i,
name: /cost \(billing period to date\)/i,
});
expect(cost).toBeInTheDocument();

View File

@@ -1,11 +1,12 @@
import { Callout } from '@signozhq/ui/callout';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import React, { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery } from 'react-query';
import { CircleCheck, Landmark, MonitorDown } from '@signozhq/icons';
import { CircleCheck, CloudDownload } from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
import {
Alert,
Button,
Card,
Col,
Flex,
@@ -15,10 +16,7 @@ import {
TableColumnsType as ColumnsType,
} from 'antd';
import { Badge } from '@signozhq/ui/badge';
import getUsage, {
BreakdownEntry,
UsageResponsePayloadProps,
} from 'api/billing/getUsage';
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
import logEvent from 'api/common/logEvent';
import updateCreditCardApi from 'api/v1/checkout/create';
import manageCreditCardApi from 'api/v1/portal/create';
@@ -31,7 +29,7 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
import { isEmpty, pick } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { ErrorResponse, SuccessResponse, SuccessResponseV2 } from 'types/api';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { getBaseUrl } from 'utils/basePath';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
@@ -40,7 +38,7 @@ import CancelSubscriptionBanner from './CancelSubscriptionBanner';
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
import { prepareCsvData } from './BillingUsageGraph/utils';
import styles from './BillingContainer.module.scss';
import './BillingContainer.styles.scss';
import { LicenseState } from 'types/api/licensesV3/getActive';
interface DataType {
@@ -117,7 +115,7 @@ const dummyColumns: ColumnsType<DataType> = [
render: renderSkeletonInput,
},
{
title: 'Cost',
title: 'Cost (Billing period to date)',
dataIndex: 'cost',
key: 'cost',
render: renderSkeletonInput,
@@ -132,7 +130,7 @@ export default function BillingContainer(): JSX.Element {
const [billAmount, setBillAmount] = useState(0);
const [daysRemaining, setDaysRemaining] = useState(0);
const [isFreeTrial, setIsFreeTrial] = useState(false);
const [data, setData] = useState<DataType[]>([]);
const [data, setData] = useState<any[]>([]);
const [apiResponse, setApiResponse] = useState<
Partial<UsageResponsePayloadProps>
>({});
@@ -152,7 +150,7 @@ export default function BillingContainer(): JSX.Element {
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const processUsageData = useCallback(
(data: SuccessResponse<UsageResponsePayloadProps> | ErrorResponse): void => {
(data: any): void => {
if (isEmpty(data?.payload)) {
return;
}
@@ -160,23 +158,27 @@ export default function BillingContainer(): JSX.Element {
details: { breakdown = [], billTotal },
billingPeriodStart,
billingPeriodEnd,
} = (data as SuccessResponse<UsageResponsePayloadProps>).payload;
const formattedUsageData: DataType[] = [];
} = data?.payload || {};
const formattedUsageData: any[] = [];
if (breakdown && Array.isArray(breakdown)) {
for (let index = 0; index < breakdown.length; index += 1) {
const element: BreakdownEntry = breakdown[index];
const element = breakdown[index];
element?.tiers?.forEach((tier, i: number) => {
formattedUsageData.push({
key: `${index}${i}`,
name: i === 0 ? element?.type : '',
unit: element?.unit ?? '',
dataIngested: `${tier.quantity} ${element?.unit}`,
pricePerUnit: String(tier.unitPrice),
cost: `$ ${tier.tierCost}`,
});
});
element?.tiers.forEach(
(
tier: { quantity: number; unitPrice: number; tierCost: number },
i: number,
) => {
formattedUsageData.push({
key: `${index}${i}`,
name: i === 0 ? element?.type : '',
dataIngested: `${tier.quantity} ${element?.unit}`,
pricePerUnit: tier.unitPrice,
cost: `$ ${tier.tierCost}`,
});
},
);
}
}
@@ -249,19 +251,16 @@ export default function BillingContainer(): JSX.Element {
title: 'Data Ingested',
dataIndex: 'dataIngested',
key: 'dataIngested',
align: 'right',
},
{
title: 'Price per Unit',
dataIndex: 'pricePerUnit',
key: 'pricePerUnit',
align: 'right',
},
{
title: 'Cost',
title: 'Cost (Billing period to date)',
dataIndex: 'cost',
key: 'cost',
align: 'right',
},
];
@@ -346,6 +345,23 @@ export default function BillingContainer(): JSX.Element {
updateCreditCard,
]);
const BillingUsageGraphCallback = useCallback(
() =>
!isLoading && !isFetchingBillingData ? (
<>
<BillingUsageGraph data={apiResponse} billAmount={billAmount} />
<div className="billing-update-note">
Note: Billing metrics are updated once every 24 hours.
</div>
</>
) : (
<Card className="empty-graph-card" bordered={false}>
<Spinner size="large" tip="Loading..." height="35vh" />
</Card>
),
[apiResponse, billAmount, isLoading, isFetchingBillingData],
);
const subscriptionPastDueMessage = (): JSX.Element => (
<Typography>
{`We were not able to process payments for your account. Please update your card details `}
@@ -399,12 +415,12 @@ export default function BillingContainer(): JSX.Element {
trialInfo?.gracePeriodEnd;
return (
<div className={styles.billingContainer}>
<Flex vertical gap={4} className={styles.pageHeader}>
<Typography.Text className={styles.pageHeaderTitle}>
<div className="billing-container">
<Flex vertical style={{ marginBottom: 16 }}>
<Typography.Text style={{ fontWeight: 500, fontSize: 18 }}>
{t('billing')}
</Typography.Text>
<Typography.Text className={styles.pageHeaderSubtitle}>
<Typography.Text color="muted">
{t('manage_billing_and_costs')}
</Typography.Text>
</Flex>
@@ -412,36 +428,50 @@ export default function BillingContainer(): JSX.Element {
<Card
bordered={false}
style={{ minHeight: 150, marginBottom: 16 }}
className={styles.pageInfo}
className="page-info"
>
<Flex justify="space-between" align="center">
<Flex vertical gap={8}>
<p className={styles.pageInfoTitle}>
<Flex vertical>
<Typography.Title level={5} style={{ marginTop: 2, fontWeight: 500 }}>
{isCloudUserVal ? t('teams_cloud') : t('teams')}{' '}
{isFreeTrial ? <Badge color="success"> Free Trial </Badge> : ''}
</p>
</Typography.Title>
{!isLoading && !isFetchingBillingData && !showGracePeriodMessage ? (
<p className={styles.pageInfoSubtitle}>
<Typography.Text style={{ fontSize: 12, color: Color.BG_VANILLA_400 }}>
{daysRemaining} {daysRemainingStr}
</p>
</Typography.Text>
) : null}
</Flex>
<Button
testId="header-billing-button"
variant="solid"
color="secondary"
size="md"
loading={isLoadingBilling || isLoadingManageBilling}
disabled={isLoading}
onClick={handleBilling}
prefix={<Landmark size={14} />}
className={styles.billingManageBtn}
>
{trialInfo?.trialConvertedToSubscription
? t('manage_billing')
: t('upgrade_plan')}
</Button>
<Flex gap={8}>
<Button
type="default"
size="middle"
loading={isLoadingBilling || isLoadingManageBilling}
disabled={isLoading || isFetchingBillingData}
onClick={handleCsvDownload}
className="periscope-btn"
>
<Flex align="center" justify="center" gap={4}>
<CloudDownload size="md" />
Download CSV
</Flex>
</Button>
<Button
data-testid="header-billing-button"
type="primary"
size="middle"
loading={isLoadingBilling || isLoadingManageBilling}
disabled={isLoading}
onClick={handleBilling}
>
{trialInfo?.trialConvertedToSubscription
? t('manage_billing')
: t('upgrade_plan')}
</Button>
<RefreshPaymentStatus type="tooltip" />
</Flex>
</Flex>
{trialInfo?.onTrial && trialInfo?.trialConvertedToSubscription && (
@@ -455,8 +485,8 @@ export default function BillingContainer(): JSX.Element {
{!isLoading && !isFetchingBillingData && !showGracePeriodMessage
? headerText && (
<Callout
title={headerText}
<Alert
message={headerText}
type="info"
showIcon
style={{ marginTop: 12 }}
@@ -473,8 +503,8 @@ export default function BillingContainer(): JSX.Element {
billingData &&
trialInfo?.gracePeriodEnd &&
showGracePeriodMessage ? (
<Callout
title={`Your data is safe with us until ${getFormattedDate(
<Alert
message={`Your data is safe with us until ${getFormattedDate(
trialInfo?.gracePeriodEnd || Date.now(),
)}. Please upgrade plan now to retain your data.`}
type="info"
@@ -485,69 +515,26 @@ export default function BillingContainer(): JSX.Element {
{isSubscriptionPastDue &&
(!isLoading && !isFetchingBillingData ? (
<Callout type="error" showIcon style={{ marginTop: 12 }}>
{subscriptionPastDueMessage()}
</Callout>
<Alert
message={subscriptionPastDueMessage()}
type="error"
showIcon
style={{ marginTop: 12 }}
/>
) : (
<Skeleton.Input active style={{ height: 20, marginTop: 20 }} />
))}
</Card>
<div className={styles.billingGraphSection}>
{!isLoading && !isFetchingBillingData ? (
<BillingUsageGraph data={apiResponse} billAmount={billAmount} />
) : (
<Card className={styles.emptyGraphCard} bordered={false}>
<Spinner size="large" tip="Loading..." height="35vh" />
</Card>
)}
{!isLoading && !isFetchingBillingData && (
<div className={styles.billingGraphFooter}>
<Button
variant="outlined"
color="secondary"
size="md"
onClick={handleCsvDownload}
prefix={<MonitorDown size={14} />}
testId="download-csv-button"
className={styles.billingFooterBtn}
>
Download CSV
</Button>
<RefreshPaymentStatus type="button" className={styles.billingFooterBtn} />
</div>
)}
</div>
{!isLoading && !isFetchingBillingData && (
<Callout type="info" size="small" className={styles.billingUpdateNote}>
Billing metrics are updated once every 24 hours.
</Callout>
)}
<BillingUsageGraphCallback />
<div className={styles.billingDetails}>
<div className="billing-details">
{!isLoading && !isFetchingBillingData && (
<Table
columns={columns}
dataSource={data}
pagination={false}
bordered={false}
components={{
header: {
cell: ({
style,
...props
}: React.ThHTMLAttributes<HTMLTableCellElement>): JSX.Element => {
const { background: _, boxShadow: __, ...safeStyle } = style ?? {};
return (
<th
{...props}
style={safeStyle}
className={`${props.className ?? ''} ${styles.billingDetailsHeaderCell}`}
/>
);
},
},
}}
/>
)}
@@ -559,7 +546,7 @@ export default function BillingContainer(): JSX.Element {
)}
{!trialInfo?.trialConvertedToSubscription && (
<div className={styles.upgradePlanBenefits}>
<div className="upgrade-plan-benefits">
<Row
justify="space-between"
align="middle"
@@ -568,16 +555,16 @@ export default function BillingContainer(): JSX.Element {
}}
gutter={[16, 16]}
>
<Col span={20} className={styles.planBenefits}>
<Typography.Text className={styles.planBenefit}>
<Col span={20} className="plan-benefits">
<Typography.Text className="plan-benefit">
<CircleCheck size="md" />
{t('upgrade_now_text')}
</Typography.Text>
<Typography.Text className={styles.planBenefit}>
<Typography.Text className="plan-benefit">
<CircleCheck size="md" />
{t('Your billing will start only after the trial period')}
</Typography.Text>
<Typography.Text className={styles.planBenefit}>
<Typography.Text className="plan-benefit">
<CircleCheck size="md" />
<span>
{t('checkout_plans')} &nbsp;
@@ -596,10 +583,9 @@ export default function BillingContainer(): JSX.Element {
</Col>
<Col span={4} style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button
testId="upgrade-plan-button"
variant="solid"
color="primary"
size="md"
data-testid="upgrade-plan-button"
type="primary"
size="middle"
loading={isLoadingBilling || isLoadingManageBilling}
onClick={handleBilling}
>

View File

@@ -1,9 +0,0 @@
.headerRow {
padding: var(--spacing-8);
}
.itemList {
overflow-y: auto;
max-height: 300px;
padding: var(--padding-3);
}

View File

@@ -1,95 +0,0 @@
import { useMemo } from 'react';
import cx from 'classnames';
import TooltipHeader from 'lib/uPlotV2/components/Tooltip/components/TooltipHeader/TooltipHeader';
import TooltipItem from 'lib/uPlotV2/components/Tooltip/components/TooltipItem/TooltipItem';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getToolTipValue } from 'components/Graph/yAxisConfig';
import { buildTooltipContent } from 'lib/uPlotV2/components/Tooltip/utils';
import {
TooltipContentItem,
TooltipRenderArgs,
} from 'lib/uPlotV2/components/types';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import TooltipStyles from 'lib/uPlotV2/components/Tooltip/Tooltip.module.scss';
import Styles from './BillingBarChartTooltip.module.scss';
interface BillingBarChartTooltipProps extends TooltipRenderArgs {
billingApiResponse: MetricRangePayloadProps;
}
const CURRENCY_SYMBOL = '$';
export function BillingBarChartTooltip({
billingApiResponse,
uPlotInstance,
dataIndexes,
seriesIndex,
isPinned,
}: BillingBarChartTooltipProps): JSX.Element {
const content = useMemo((): TooltipContentItem[] => {
const baseItems = buildTooltipContent({
data: uPlotInstance.data,
series: uPlotInstance.series,
dataIndexes,
activeSeriesIndex: seriesIndex,
uPlotInstance,
yAxisUnit: '',
isStackedBarChart: true,
});
return baseItems.map((item) => {
const match = billingApiResponse.data.result.find(
(r) => (r.legend || r.queryName) === item.label,
);
if (!match) {
return item;
}
const seriesIdx = uPlotInstance.series.findIndex(
(s) => s.label === item.label,
);
if (seriesIdx === -1) {
return item;
}
const dataIndex = dataIndexes[seriesIdx];
const quantity = dataIndex != null ? match.quantity?.[dataIndex] : null;
const unit = match.unit ?? '';
const quantityStr =
quantity != null ? ` - ${getToolTipValue(quantity)} ${unit}` : '';
return {
...item,
tooltipValue: `${CURRENCY_SYMBOL}${getToolTipValue(item.value, '')}${quantityStr}`,
};
});
}, [uPlotInstance, seriesIndex, dataIndexes, billingApiResponse]);
const activeItem = content.find((item) => item.isActive) ?? null;
return (
<div
className={cx(TooltipStyles.container, {
[TooltipStyles.pinned]: isPinned,
})}
data-testid="uplot-tooltip-container"
>
<TooltipHeader
uPlotInstance={uPlotInstance}
showTooltipHeader
isPinned={isPinned}
activeItem={null}
headerRowClassName={Styles.headerRow}
dateFormat={DATE_TIME_FORMATS.MONTH_DATE}
/>
{activeItem != null && <span className={TooltipStyles.divider} />}
<div className={Styles.itemList} data-testid="uplot-tooltip-list">
{content.map((item) => (
<TooltipItem key={item.label} item={item} isItemActive={item.isActive} />
))}
</div>
</div>
);
}

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