mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-08 18:10:27 +01:00
Compare commits
41 Commits
refactor/a
...
fts-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bac933968 | ||
|
|
8df70ed4be | ||
|
|
806f1d0062 | ||
|
|
19d7d5b2ad | ||
|
|
2f762a86b3 | ||
|
|
8861f4523c | ||
|
|
7f97249de3 | ||
|
|
da9b7b25ed | ||
|
|
0546050b82 | ||
|
|
b8de25e2eb | ||
|
|
8f77b5451c | ||
|
|
a55cab858d | ||
|
|
8f4a48ee51 | ||
|
|
c1d788f59e | ||
|
|
cd720beb7d | ||
|
|
63c7b24eac | ||
|
|
c5752829f7 | ||
|
|
404c10685d | ||
|
|
e7c83604de | ||
|
|
3b17f31948 | ||
|
|
ba8c83f47e | ||
|
|
ad2c46eebb | ||
|
|
f9110873f8 | ||
|
|
70bbfdd937 | ||
|
|
10a8db013b | ||
|
|
249d34a9dc | ||
|
|
9b5ad29794 | ||
|
|
90c2622da9 | ||
|
|
36b36fa80d | ||
|
|
5a547cf358 | ||
|
|
937c3f9359 | ||
|
|
a898225737 | ||
|
|
81f382e353 | ||
|
|
ef0ab2fe8e | ||
|
|
6c649d35cb | ||
|
|
d5c3fe1651 | ||
|
|
83c43ece31 | ||
|
|
a87654a614 | ||
|
|
952f5d6e91 | ||
|
|
2154ea30a6 | ||
|
|
6e3857b840 |
23
.github/CODEOWNERS
vendored
23
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/build-enterprise.yaml
vendored
2
.github/workflows/build-enterprise.yaml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/build-staging.yaml
vendored
2
.github/workflows/build-staging.yaml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/gor-signoz.yaml
vendored
2
.github/workflows/gor-signoz.yaml
vendored
@@ -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:
|
||||
|
||||
4
.github/workflows/integrationci.yaml
vendored
4
.github/workflows/integrationci.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -64,16 +64,16 @@ web:
|
||||
settings:
|
||||
posthog:
|
||||
# Whether to enable PostHog in web.
|
||||
enabled: false
|
||||
enabled: true
|
||||
appcues:
|
||||
# Whether to enable Appcues in web.
|
||||
enabled: false
|
||||
enabled: true
|
||||
sentry:
|
||||
# Whether to enable Sentry in web.
|
||||
enabled: false
|
||||
enabled: true
|
||||
pylon:
|
||||
# Whether to enable Pylon in web.
|
||||
enabled: false
|
||||
enabled: true
|
||||
|
||||
##################### Cache #####################
|
||||
cache:
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -96,19 +96,6 @@ components:
|
||||
- createdAt
|
||||
- updatedAt
|
||||
type: object
|
||||
AlertmanagertypesGoogleChatReceiverConfig:
|
||||
properties:
|
||||
http_config:
|
||||
$ref: '#/components/schemas/ConfigHTTPClientConfig'
|
||||
send_resolved:
|
||||
type: boolean
|
||||
text:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
webhook_url:
|
||||
$ref: '#/components/schemas/ConfigSecretURL'
|
||||
type: object
|
||||
AlertmanagertypesMaintenanceKind:
|
||||
enum:
|
||||
- fixed
|
||||
@@ -160,8 +147,6 @@ components:
|
||||
type: object
|
||||
AlertmanagertypesPostableChannel:
|
||||
oneOf:
|
||||
- required:
|
||||
- googlechat_configs
|
||||
- required:
|
||||
- discord_configs
|
||||
- required:
|
||||
@@ -207,10 +192,6 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigEmailConfig'
|
||||
type: array
|
||||
googlechat_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/AlertmanagertypesGoogleChatReceiverConfig'
|
||||
type: array
|
||||
incidentio_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigIncidentioConfig'
|
||||
@@ -324,87 +305,6 @@ components:
|
||||
- channels
|
||||
- name
|
||||
type: object
|
||||
AlertmanagertypesReceiver:
|
||||
properties:
|
||||
discord_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigDiscordConfig'
|
||||
type: array
|
||||
email_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigEmailConfig'
|
||||
type: array
|
||||
googlechat_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/AlertmanagertypesGoogleChatReceiverConfig'
|
||||
type: array
|
||||
incidentio_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigIncidentioConfig'
|
||||
type: array
|
||||
jira_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigJiraConfig'
|
||||
type: array
|
||||
mattermost_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMattermostConfig'
|
||||
type: array
|
||||
msteams_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMSTeamsConfig'
|
||||
type: array
|
||||
msteamsv2_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMSTeamsV2Config'
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
opsgenie_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigOpsGenieConfig'
|
||||
type: array
|
||||
pagerduty_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigPagerdutyConfig'
|
||||
type: array
|
||||
pushover_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigPushoverConfig'
|
||||
type: array
|
||||
rocketchat_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigRocketchatConfig'
|
||||
type: array
|
||||
slack_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigSlackConfig'
|
||||
type: array
|
||||
sns_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigSNSConfig'
|
||||
type: array
|
||||
telegram_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigTelegramConfig'
|
||||
type: array
|
||||
victorops_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigVictorOpsConfig'
|
||||
type: array
|
||||
webex_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWebexConfig'
|
||||
type: array
|
||||
webhook_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWebhookConfig'
|
||||
type: array
|
||||
wechat_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWechatConfig'
|
||||
type: array
|
||||
type: object
|
||||
AlertmanagertypesRecurrence:
|
||||
properties:
|
||||
duration:
|
||||
@@ -970,6 +870,14 @@ components:
|
||||
- timestampMillis
|
||||
- data
|
||||
type: object
|
||||
CloudintegrationtypesAssets:
|
||||
properties:
|
||||
dashboards:
|
||||
items:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesDashboard'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
CloudintegrationtypesAzureAccountConfig:
|
||||
properties:
|
||||
deploymentRegion:
|
||||
@@ -1117,6 +1025,17 @@ components:
|
||||
- ingestionUrl
|
||||
- ingestionKey
|
||||
type: object
|
||||
CloudintegrationtypesDashboard:
|
||||
properties:
|
||||
definition:
|
||||
$ref: '#/components/schemas/DashboardtypesStorableDashboardData'
|
||||
description:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
type: object
|
||||
CloudintegrationtypesDataCollected:
|
||||
properties:
|
||||
logs:
|
||||
@@ -1290,7 +1209,7 @@ components:
|
||||
CloudintegrationtypesService:
|
||||
properties:
|
||||
assets:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesServiceAssets'
|
||||
$ref: '#/components/schemas/CloudintegrationtypesAssets'
|
||||
cloudIntegrationService:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesCloudIntegrationService'
|
||||
dataCollected:
|
||||
@@ -1303,6 +1222,8 @@ components:
|
||||
type: string
|
||||
supportedSignals:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesSupportedSignals'
|
||||
telemetryCollectionStrategy:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesTelemetryCollectionStrategy'
|
||||
title:
|
||||
type: string
|
||||
required:
|
||||
@@ -1313,17 +1234,9 @@ components:
|
||||
- assets
|
||||
- supportedSignals
|
||||
- dataCollected
|
||||
- telemetryCollectionStrategy
|
||||
- cloudIntegrationService
|
||||
type: object
|
||||
CloudintegrationtypesServiceAssets:
|
||||
properties:
|
||||
dashboards:
|
||||
items:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesServiceDashboard'
|
||||
type: array
|
||||
required:
|
||||
- dashboards
|
||||
type: object
|
||||
CloudintegrationtypesServiceConfig:
|
||||
properties:
|
||||
aws:
|
||||
@@ -1331,18 +1244,6 @@ components:
|
||||
azure:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesAzureServiceConfig'
|
||||
type: object
|
||||
CloudintegrationtypesServiceDashboard:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
integrationDashboard:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesStorableIntegrationDashboard'
|
||||
title:
|
||||
type: string
|
||||
required:
|
||||
- title
|
||||
- description
|
||||
type: object
|
||||
CloudintegrationtypesServiceID:
|
||||
enum:
|
||||
- alb
|
||||
@@ -1377,30 +1278,6 @@ components:
|
||||
- icon
|
||||
- enabled
|
||||
type: object
|
||||
CloudintegrationtypesStorableIntegrationDashboard:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
dashboardId:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
provider:
|
||||
type: string
|
||||
slug:
|
||||
type: string
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- dashboardId
|
||||
- provider
|
||||
- slug
|
||||
- createdAt
|
||||
- updatedAt
|
||||
type: object
|
||||
CloudintegrationtypesSupportedSignals:
|
||||
properties:
|
||||
logs:
|
||||
@@ -1408,6 +1285,13 @@ components:
|
||||
metrics:
|
||||
type: boolean
|
||||
type: object
|
||||
CloudintegrationtypesTelemetryCollectionStrategy:
|
||||
properties:
|
||||
aws:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesAWSTelemetryCollectionStrategy'
|
||||
azure:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesAzureTelemetryCollectionStrategy'
|
||||
type: object
|
||||
CloudintegrationtypesUpdatableAccount:
|
||||
properties:
|
||||
config:
|
||||
@@ -1965,6 +1849,83 @@ components:
|
||||
user_key_file:
|
||||
type: string
|
||||
type: object
|
||||
ConfigReceiver:
|
||||
properties:
|
||||
discord_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigDiscordConfig'
|
||||
type: array
|
||||
email_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigEmailConfig'
|
||||
type: array
|
||||
incidentio_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigIncidentioConfig'
|
||||
type: array
|
||||
jira_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigJiraConfig'
|
||||
type: array
|
||||
mattermost_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMattermostConfig'
|
||||
type: array
|
||||
msteams_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMSTeamsConfig'
|
||||
type: array
|
||||
msteamsv2_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMSTeamsV2Config'
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
opsgenie_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigOpsGenieConfig'
|
||||
type: array
|
||||
pagerduty_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigPagerdutyConfig'
|
||||
type: array
|
||||
pushover_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigPushoverConfig'
|
||||
type: array
|
||||
rocketchat_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigRocketchatConfig'
|
||||
type: array
|
||||
slack_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigSlackConfig'
|
||||
type: array
|
||||
sns_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigSNSConfig'
|
||||
type: array
|
||||
telegram_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigTelegramConfig'
|
||||
type: array
|
||||
victorops_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigVictorOpsConfig'
|
||||
type: array
|
||||
webex_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWebexConfig'
|
||||
type: array
|
||||
webhook_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWebhookConfig'
|
||||
type: array
|
||||
wechat_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWechatConfig'
|
||||
type: array
|
||||
type: object
|
||||
ConfigRocketchatAttachmentAction:
|
||||
properties:
|
||||
image_url:
|
||||
@@ -2419,26 +2380,6 @@ components:
|
||||
repeatVariable:
|
||||
type: string
|
||||
type: object
|
||||
DashboardLink:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
renderVariables:
|
||||
type: boolean
|
||||
targetBlank:
|
||||
type: boolean
|
||||
tooltip:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
DashboardPanelDisplay:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
DashboardTextVariableSpec:
|
||||
properties:
|
||||
constant:
|
||||
@@ -2569,7 +2510,7 @@ components:
|
||||
type: array
|
||||
links:
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardLink'
|
||||
$ref: '#/components/schemas/V1Link'
|
||||
type: array
|
||||
panels:
|
||||
additionalProperties:
|
||||
@@ -2704,34 +2645,10 @@ components:
|
||||
legend:
|
||||
$ref: '#/components/schemas/DashboardtypesLegend'
|
||||
type: object
|
||||
DashboardtypesJSONPatchOperation:
|
||||
properties:
|
||||
from:
|
||||
description: Source JSON Pointer for move/copy ops; ignored for other ops.
|
||||
type: string
|
||||
op:
|
||||
$ref: '#/components/schemas/DashboardtypesPatchOp'
|
||||
path:
|
||||
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/-.
|
||||
type: string
|
||||
value:
|
||||
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.'
|
||||
required:
|
||||
- op
|
||||
- path
|
||||
type: object
|
||||
DashboardtypesLayout:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/DashboardtypesLayoutEnvelopeGithubComPersesSpecGoDashboardGridLayoutSpec'
|
||||
DashboardtypesLayoutEnvelopeGithubComPersesSpecGoDashboardGridLayoutSpec:
|
||||
- $ref: '#/components/schemas/DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpec'
|
||||
DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
@@ -2814,7 +2731,7 @@ components:
|
||||
DashboardtypesPanel:
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/DashboardtypesPanelKind'
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/DashboardtypesPanelSpec'
|
||||
type: object
|
||||
@@ -2825,10 +2742,6 @@ components:
|
||||
unit:
|
||||
type: string
|
||||
type: object
|
||||
DashboardtypesPanelKind:
|
||||
enum:
|
||||
- Panel
|
||||
type: string
|
||||
DashboardtypesPanelPlugin:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec'
|
||||
@@ -2935,10 +2848,10 @@ components:
|
||||
DashboardtypesPanelSpec:
|
||||
properties:
|
||||
display:
|
||||
$ref: '#/components/schemas/DashboardPanelDisplay'
|
||||
$ref: '#/components/schemas/V1PanelDisplay'
|
||||
links:
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardLink'
|
||||
$ref: '#/components/schemas/V1Link'
|
||||
type: array
|
||||
plugin:
|
||||
$ref: '#/components/schemas/DashboardtypesPanelPlugin'
|
||||
@@ -2947,20 +2860,6 @@ components:
|
||||
$ref: '#/components/schemas/DashboardtypesQuery'
|
||||
type: array
|
||||
type: object
|
||||
DashboardtypesPatchOp:
|
||||
enum:
|
||||
- add
|
||||
- remove
|
||||
- replace
|
||||
- move
|
||||
- copy
|
||||
- test
|
||||
type: string
|
||||
DashboardtypesPatchableDashboardV2:
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardtypesJSONPatchOperation'
|
||||
nullable: true
|
||||
type: array
|
||||
DashboardtypesPieChartPanelSpec:
|
||||
properties:
|
||||
formatting:
|
||||
@@ -3011,7 +2910,7 @@ components:
|
||||
DashboardtypesQuery:
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5RequestType'
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/DashboardtypesQuerySpec'
|
||||
type: object
|
||||
@@ -3248,27 +3147,6 @@ components:
|
||||
timePreference:
|
||||
$ref: '#/components/schemas/DashboardtypesTimePreference'
|
||||
type: object
|
||||
DashboardtypesUpdatableDashboardV2:
|
||||
properties:
|
||||
image:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
schemaVersion:
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/DashboardtypesDashboardSpec'
|
||||
tags:
|
||||
items:
|
||||
$ref: '#/components/schemas/TagtypesPostableTag'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- schemaVersion
|
||||
- name
|
||||
- tags
|
||||
- spec
|
||||
type: object
|
||||
DashboardtypesUpdatablePublicDashboard:
|
||||
properties:
|
||||
defaultTimeRange:
|
||||
@@ -3279,8 +3157,8 @@ components:
|
||||
DashboardtypesVariable:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/DashboardtypesVariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesListVariableSpec'
|
||||
- $ref: '#/components/schemas/DashboardtypesVariableEnvelopeGithubComPersesSpecGoDashboardTextVariableSpec'
|
||||
DashboardtypesVariableEnvelopeGithubComPersesSpecGoDashboardTextVariableSpec:
|
||||
- $ref: '#/components/schemas/DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpec'
|
||||
DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
@@ -7298,6 +7176,26 @@ components:
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
V1Link:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
renderVariables:
|
||||
type: boolean
|
||||
targetBlank:
|
||||
type: boolean
|
||||
tooltip:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
V1PanelDisplay:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
VariableDefaultValue:
|
||||
type: object
|
||||
VariableDisplay:
|
||||
@@ -7714,7 +7612,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AlertmanagertypesReceiver'
|
||||
$ref: '#/components/schemas/ConfigReceiver'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
@@ -7765,7 +7663,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AlertmanagertypesReceiver'
|
||||
$ref: '#/components/schemas/ConfigReceiver'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
@@ -8130,139 +8028,7 @@ paths:
|
||||
summary: Update account
|
||||
tags:
|
||||
- cloudintegration
|
||||
/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}/services:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint lists the services metadata for the specified account
|
||||
and cloud provider
|
||||
operationId: ListAccountServicesMetadata
|
||||
parameters:
|
||||
- in: path
|
||||
name: cloud_provider
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesGettableServicesMetadata'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: List account services metadata
|
||||
tags:
|
||||
- cloudintegration
|
||||
/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}/services/{service_id}:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint gets a service and its configuration for the specified
|
||||
cloud integration account
|
||||
operationId: GetAccountService
|
||||
parameters:
|
||||
- in: path
|
||||
name: cloud_provider
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: service_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesService'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Get service for account
|
||||
tags:
|
||||
- cloudintegration
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint updates a service for the specified cloud provider
|
||||
@@ -12503,7 +12269,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AlertmanagertypesReceiver'
|
||||
$ref: '#/components/schemas/ConfigReceiver'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
@@ -13058,12 +12824,6 @@ paths:
|
||||
- data
|
||||
type: object
|
||||
description: Created
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
@@ -13116,12 +12876,6 @@ paths:
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
@@ -13134,12 +12888,6 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
@@ -13154,262 +12902,6 @@ paths:
|
||||
summary: Get dashboard (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
patch:
|
||||
deprecated: false
|
||||
description: '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.'
|
||||
operationId: PatchDashboardV2
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DashboardtypesPatchableDashboardV2'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesGettableDashboardV2'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Patch dashboard (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint updates a v2-shape dashboard's metadata, data, and
|
||||
tag set. Locked dashboards are rejected.
|
||||
operationId: UpdateDashboardV2
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DashboardtypesUpdatableDashboardV2'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesGettableDashboardV2'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Update dashboard (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/dashboards/{id}/lock:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: This endpoint unlocks a v2-shape dashboard. Only the dashboard's
|
||||
creator or an org admin may lock or unlock.
|
||||
operationId: UnlockDashboardV2
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Unlock dashboard (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint locks a v2-shape dashboard. Only the dashboard's
|
||||
creator or an org admin may lock or unlock.
|
||||
operationId: LockDashboardV2
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Lock dashboard (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/factor_password/forgot:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
28
frontend/__mocks__/useSafeNavigate.ts
Normal file
28
frontend/__mocks__/useSafeNavigate.ts
Normal 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
|
||||
>,
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -8717,31 +8623,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 +9476,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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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) => {
|
||||
|
||||
1
frontend/src/assets/Icons/solid-x-circle.svg
Normal file
1
frontend/src/assets/Icons/solid-x-circle.svg
Normal 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 |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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() }));
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.alertHistory {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-10);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
.container {
|
||||
.alert-history {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 24px;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
height: 280px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 4px;
|
||||
margin: 0 var(--spacing-8);
|
||||
margin: 0 16px;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
.topContributorsCardViewAll {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.topContributorsCardContent {
|
||||
:global(.ant-table-cell) {
|
||||
padding: var(--spacing-6) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.viewAllDrawer {
|
||||
border-radius: 4px;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 }}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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')}
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.headerRow {
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.itemList {
|
||||
overflow-y: auto;
|
||||
max-height: 300px;
|
||||
padding: var(--padding-3);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
.graphContainer {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.billingGraphCard {
|
||||
:global {
|
||||
.uplot-no-data {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
height: 40vh;
|
||||
|
||||
.uplot-graph-container {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.totalSpent {
|
||||
font-family: 'SF Mono', monospace;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.totalSpentTitle {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.48px;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user