mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-10 05:20:40 +01:00
Compare commits
21 Commits
proto/stor
...
issue_5329
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a849eac876 | ||
|
|
5258f5e8e3 | ||
|
|
8292988751 | ||
|
|
6e96a5ce37 | ||
|
|
d5af6f6d6b | ||
|
|
e7c48e964f | ||
|
|
fe8c9d8cf4 | ||
|
|
2ad3057fa6 | ||
|
|
9fc73fc0b1 | ||
|
|
3931c7163f | ||
|
|
fdfbf77d2b | ||
|
|
56e7388677 | ||
|
|
861380dc65 | ||
|
|
391f685e57 | ||
|
|
e0da06f76d | ||
|
|
85f9924b4b | ||
|
|
c015622258 | ||
|
|
0f36cb9334 | ||
|
|
e1ee386016 | ||
|
|
717d37a945 | ||
|
|
63b130ccd1 |
28
.github/CODEOWNERS
vendored
28
.github/CODEOWNERS
vendored
@@ -152,39 +152,29 @@ go.mod @therealpandey
|
||||
|
||||
## Dashboard Types
|
||||
|
||||
/frontend/src/api/types/dashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/types/api/dashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/types/api/widgets/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard List
|
||||
## Widget Card
|
||||
|
||||
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
|
||||
|
||||
# Dashboard Widget Page
|
||||
|
||||
/frontend/src/pages/DashboardWidget/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard Page
|
||||
|
||||
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/WidgetCard/ @SigNoz/pulse-frontend
|
||||
|
||||
## Public Dashboard Page
|
||||
|
||||
/frontend/src/pages/PublicDashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/PublicDashboardContainer/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard Libs + Components
|
||||
|
||||
/frontend/src/lib/uPlotV2/ @SigNoz/pulse-frontend
|
||||
/frontend/src/lib/visualization/ @SigNoz/pulse-frontend
|
||||
/frontend/src/lib/dashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/lib/dashboardVariables/ @SigNoz/pulse-frontend
|
||||
/frontend/src/components/NewSelect/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard V2
|
||||
/frontend/src/pages/DashboardPageV2/ @SigNoz/pulse-frontend
|
||||
/frontend/src/pages/DashboardsListPageV2/ @SigNoz/pulse-frontend
|
||||
## Dashboard Pages
|
||||
|
||||
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
||||
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
||||
|
||||
## Infrastructure Monitoring
|
||||
/frontend/src/pages/InfrastructureMonitoring/ @SigNoz/pulse-frontend
|
||||
|
||||
@@ -97,6 +97,7 @@ func runGenerateAuthz(_ context.Context) error {
|
||||
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceFactorAPIKey).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceLicense).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceSubscription).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceLogs).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceTraces).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceMetrics).String(): true,
|
||||
|
||||
@@ -44,6 +44,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/subscription"
|
||||
"github.com/SigNoz/signoz/pkg/subscription/noopsubscription"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
@@ -87,6 +89,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(_ sqlstore.SQLStore, _ zeus.Zeus, _ organization.Getter, _ analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
|
||||
return nooplicensing.NewFactory()
|
||||
},
|
||||
func(_ zeus.Zeus, _ licensing.Licensing) subscription.Subscription {
|
||||
return noopsubscription.New()
|
||||
},
|
||||
signoz.NewEmailingProviderFactories(),
|
||||
signoz.NewCacheProviderFactories(),
|
||||
signoz.NewWebProviderFactories(config.Global),
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
eequerier "github.com/SigNoz/signoz/ee/querier"
|
||||
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
|
||||
eerules "github.com/SigNoz/signoz/ee/query-service/rules"
|
||||
"github.com/SigNoz/signoz/ee/subscription/httpsubscription"
|
||||
enterprisezeus "github.com/SigNoz/signoz/ee/zeus"
|
||||
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
@@ -60,6 +61,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/subscription"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
@@ -103,6 +105,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
|
||||
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter, analytics)
|
||||
},
|
||||
func(zeus zeus.Zeus, licensing licensing.Licensing) subscription.Subscription {
|
||||
return httpsubscription.New(zeus, licensing)
|
||||
},
|
||||
signoz.NewEmailingProviderFactories(),
|
||||
signoz.NewCacheProviderFactories(),
|
||||
signoz.NewWebProviderFactories(config.Global),
|
||||
|
||||
@@ -397,7 +397,6 @@ identn:
|
||||
# headers to use for tokenizer identN resolver
|
||||
headers:
|
||||
- Authorization
|
||||
- Sec-WebSocket-Protocol
|
||||
apikey:
|
||||
# toggle apikey identN
|
||||
enabled: true
|
||||
|
||||
@@ -25,6 +25,379 @@ components:
|
||||
- data
|
||||
- orgId
|
||||
type: object
|
||||
AlertmanagertypesChannelConfig:
|
||||
discriminator:
|
||||
mapping:
|
||||
email: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelEmailConfig'
|
||||
googlechat: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelGoogleChatConfig'
|
||||
incidentio: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelIncidentIOConfig'
|
||||
jira: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJiraConfig'
|
||||
jsmops: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJSMOpsConfig'
|
||||
msteams: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelMSTeamsConfig'
|
||||
opsgenie: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelOpsgenieConfig'
|
||||
pagerduty: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelPagerdutyConfig'
|
||||
slack: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelSlackConfig'
|
||||
webhook: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelWebhookConfig'
|
||||
propertyName: kind
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelSlackConfig'
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelEmailConfig'
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelWebhookConfig'
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelPagerdutyConfig'
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelOpsgenieConfig'
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelMSTeamsConfig'
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelGoogleChatConfig'
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJiraConfig'
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJSMOpsConfig'
|
||||
- $ref: '#/components/schemas/AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelIncidentIOConfig'
|
||||
type: object
|
||||
AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelEmailConfig:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- email
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelEmailConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelGoogleChatConfig:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- googlechat
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelGoogleChatConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelIncidentIOConfig:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- incidentio
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelIncidentIOConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJSMOpsConfig:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- jsmops
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelJSMOpsConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJiraConfig:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- jira
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelJiraConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelMSTeamsConfig:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- msteams
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelMSTeamsConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelOpsgenieConfig:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- opsgenie
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelOpsgenieConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelPagerdutyConfig:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- pagerduty
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelPagerdutyConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelSlackConfig:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- slack
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelSlackConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelWebhookConfig:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- webhook
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelWebhookConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AlertmanagertypesChannelEmailConfig:
|
||||
properties:
|
||||
headers:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
html:
|
||||
type: string
|
||||
sendResolved:
|
||||
nullable: true
|
||||
type: boolean
|
||||
to:
|
||||
type: string
|
||||
required:
|
||||
- to
|
||||
type: object
|
||||
AlertmanagertypesChannelGoogleChatConfig:
|
||||
properties:
|
||||
sendResolved:
|
||||
nullable: true
|
||||
type: boolean
|
||||
text:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
webhookUrl:
|
||||
type: string
|
||||
required:
|
||||
- webhookUrl
|
||||
type: object
|
||||
AlertmanagertypesChannelIncidentIOConfig:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
metadata:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
sendResolved:
|
||||
nullable: true
|
||||
type: boolean
|
||||
title:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
- url
|
||||
- token
|
||||
type: object
|
||||
AlertmanagertypesChannelJSMOpsConfig:
|
||||
properties:
|
||||
apiKey:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
priority:
|
||||
type: string
|
||||
sendResolved:
|
||||
nullable: true
|
||||
type: boolean
|
||||
tags:
|
||||
type: string
|
||||
required:
|
||||
- apiKey
|
||||
type: object
|
||||
AlertmanagertypesChannelJiraConfig:
|
||||
properties:
|
||||
apiToken:
|
||||
type: string
|
||||
customFields:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
description:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
issueType:
|
||||
type: string
|
||||
labels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
priority:
|
||||
type: string
|
||||
project:
|
||||
type: string
|
||||
reopenDuration:
|
||||
type: string
|
||||
reopenTransition:
|
||||
type: string
|
||||
resolveTransition:
|
||||
type: string
|
||||
sendResolved:
|
||||
nullable: true
|
||||
type: boolean
|
||||
site:
|
||||
type: string
|
||||
summary:
|
||||
type: string
|
||||
wontFixResolution:
|
||||
type: string
|
||||
required:
|
||||
- site
|
||||
- project
|
||||
- issueType
|
||||
- email
|
||||
- apiToken
|
||||
type: object
|
||||
AlertmanagertypesChannelKind:
|
||||
enum:
|
||||
- slack
|
||||
- email
|
||||
- webhook
|
||||
- pagerduty
|
||||
- opsgenie
|
||||
- msteams
|
||||
- googlechat
|
||||
- jira
|
||||
- jsmops
|
||||
- incidentio
|
||||
type: string
|
||||
AlertmanagertypesChannelMSTeamsConfig:
|
||||
properties:
|
||||
sendResolved:
|
||||
nullable: true
|
||||
type: boolean
|
||||
text:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
webhookUrl:
|
||||
type: string
|
||||
required:
|
||||
- webhookUrl
|
||||
type: object
|
||||
AlertmanagertypesChannelOpsgenieConfig:
|
||||
properties:
|
||||
apiKey:
|
||||
type: string
|
||||
apiUrl:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
details:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
message:
|
||||
type: string
|
||||
priority:
|
||||
type: string
|
||||
sendResolved:
|
||||
nullable: true
|
||||
type: boolean
|
||||
source:
|
||||
type: string
|
||||
required:
|
||||
- apiKey
|
||||
type: object
|
||||
AlertmanagertypesChannelPagerdutyConfig:
|
||||
properties:
|
||||
class:
|
||||
type: string
|
||||
client:
|
||||
type: string
|
||||
clientUrl:
|
||||
type: string
|
||||
component:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
details:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
group:
|
||||
type: string
|
||||
routingKey:
|
||||
type: string
|
||||
sendResolved:
|
||||
nullable: true
|
||||
type: boolean
|
||||
severity:
|
||||
type: string
|
||||
source:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
- routingKey
|
||||
type: object
|
||||
AlertmanagertypesChannelSlackConfig:
|
||||
properties:
|
||||
apiUrl:
|
||||
type: string
|
||||
channel:
|
||||
type: string
|
||||
sendResolved:
|
||||
nullable: true
|
||||
type: boolean
|
||||
text:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
required:
|
||||
- apiUrl
|
||||
type: object
|
||||
AlertmanagertypesChannelWebhookConfig:
|
||||
properties:
|
||||
bearerToken:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
sendResolved:
|
||||
nullable: true
|
||||
type: boolean
|
||||
url:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
required:
|
||||
- url
|
||||
type: object
|
||||
AlertmanagertypesDeprecatedGettableAlert:
|
||||
properties:
|
||||
annotations:
|
||||
@@ -54,6 +427,30 @@ components:
|
||||
- rule
|
||||
- policy
|
||||
type: string
|
||||
AlertmanagertypesGettableNotificationChannel:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelConfig'
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- displayName
|
||||
- config
|
||||
- id
|
||||
- createdAt
|
||||
- updatedAt
|
||||
type: object
|
||||
AlertmanagertypesGettableRoutePolicy:
|
||||
properties:
|
||||
channels:
|
||||
@@ -356,6 +753,19 @@ components:
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
AlertmanagertypesPostableNotificationChannel:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/components/schemas/AlertmanagertypesChannelConfig'
|
||||
displayName:
|
||||
type: string
|
||||
generateName:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- config
|
||||
type: object
|
||||
AlertmanagertypesPostablePlannedMaintenance:
|
||||
properties:
|
||||
alertIds:
|
||||
@@ -9017,6 +9427,8 @@ components:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
origin:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
@@ -9029,6 +9441,7 @@ components:
|
||||
- fieldContext
|
||||
- config
|
||||
- enabled
|
||||
- origin
|
||||
type: object
|
||||
SpantypesSpanMapperConfig:
|
||||
properties:
|
||||
@@ -9057,48 +9470,75 @@ components:
|
||||
type: string
|
||||
orgId:
|
||||
type: string
|
||||
origin:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
version:
|
||||
type: integer
|
||||
required:
|
||||
- id
|
||||
- orgId
|
||||
- name
|
||||
- condition
|
||||
- enabled
|
||||
- origin
|
||||
- version
|
||||
type: object
|
||||
SpantypesSpanMapperGroupCondition:
|
||||
nullable: true
|
||||
properties:
|
||||
attributes:
|
||||
items:
|
||||
type: string
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperGroupConditionKey'
|
||||
nullable: true
|
||||
type: array
|
||||
resource:
|
||||
items:
|
||||
type: string
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperGroupConditionKey'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- attributes
|
||||
- resource
|
||||
type: object
|
||||
SpantypesSpanMapperGroupConditionKey:
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
origin:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
|
||||
value:
|
||||
type: string
|
||||
required:
|
||||
- value
|
||||
- enabled
|
||||
type: object
|
||||
SpantypesSpanMapperOperation:
|
||||
enum:
|
||||
- move
|
||||
- copy
|
||||
type: string
|
||||
SpantypesSpanMapperOrigin:
|
||||
enum:
|
||||
- user
|
||||
- system
|
||||
type: string
|
||||
SpantypesSpanMapperSource:
|
||||
properties:
|
||||
context:
|
||||
$ref: '#/components/schemas/SpantypesFieldContext'
|
||||
enabled:
|
||||
type: boolean
|
||||
key:
|
||||
type: string
|
||||
operation:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperOperation'
|
||||
origin:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
|
||||
priority:
|
||||
type: integer
|
||||
required:
|
||||
@@ -9106,6 +9546,7 @@ components:
|
||||
- context
|
||||
- operation
|
||||
- priority
|
||||
- enabled
|
||||
type: object
|
||||
SpantypesSpanMapperTestSpan:
|
||||
properties:
|
||||
@@ -9217,6 +9658,116 @@ components:
|
||||
required:
|
||||
- references
|
||||
type: object
|
||||
SubscriptiontypesGettableSubscription:
|
||||
properties:
|
||||
redirectURL:
|
||||
type: string
|
||||
required:
|
||||
- redirectURL
|
||||
type: object
|
||||
SubscriptiontypesGettableSubscriptionUsage:
|
||||
properties:
|
||||
billingPeriodEnd:
|
||||
format: int64
|
||||
type: integer
|
||||
billingPeriodStart:
|
||||
format: int64
|
||||
type: integer
|
||||
details:
|
||||
$ref: '#/components/schemas/SubscriptiontypesSubscriptionUsageDetails'
|
||||
discount:
|
||||
format: double
|
||||
type: number
|
||||
subscriptionStatus:
|
||||
type: string
|
||||
type: object
|
||||
SubscriptiontypesPostableSubscription:
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
- url
|
||||
type: object
|
||||
SubscriptiontypesSubscriptionUsageBreakdown:
|
||||
properties:
|
||||
dayWiseBreakdown:
|
||||
$ref: '#/components/schemas/SubscriptiontypesSubscriptionUsageDayWiseBreakdown'
|
||||
tiers:
|
||||
items:
|
||||
$ref: '#/components/schemas/SubscriptiontypesSubscriptionUsageTier'
|
||||
nullable: true
|
||||
type: array
|
||||
type:
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
type: object
|
||||
SubscriptiontypesSubscriptionUsageDayWiseBreakdown:
|
||||
properties:
|
||||
breakdown:
|
||||
items:
|
||||
$ref: '#/components/schemas/SubscriptiontypesSubscriptionUsageDayWiseData'
|
||||
nullable: true
|
||||
type: array
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
SubscriptiontypesSubscriptionUsageDayWiseData:
|
||||
properties:
|
||||
count:
|
||||
format: double
|
||||
type: number
|
||||
quantity:
|
||||
format: double
|
||||
type: number
|
||||
size:
|
||||
format: double
|
||||
type: number
|
||||
timestamp:
|
||||
format: int64
|
||||
type: integer
|
||||
total:
|
||||
format: double
|
||||
type: number
|
||||
unitPrice:
|
||||
format: double
|
||||
type: number
|
||||
type: object
|
||||
SubscriptiontypesSubscriptionUsageDetails:
|
||||
properties:
|
||||
baseFee:
|
||||
format: double
|
||||
type: number
|
||||
billTotal:
|
||||
format: double
|
||||
type: number
|
||||
breakdown:
|
||||
items:
|
||||
$ref: '#/components/schemas/SubscriptiontypesSubscriptionUsageBreakdown'
|
||||
nullable: true
|
||||
type: array
|
||||
total:
|
||||
format: double
|
||||
type: number
|
||||
type: object
|
||||
SubscriptiontypesSubscriptionUsageTier:
|
||||
properties:
|
||||
quantity:
|
||||
format: double
|
||||
type: number
|
||||
tierCost:
|
||||
format: double
|
||||
type: number
|
||||
tierEnd:
|
||||
format: int64
|
||||
type: integer
|
||||
tierStart:
|
||||
format: int64
|
||||
type: integer
|
||||
unitPrice:
|
||||
format: double
|
||||
type: number
|
||||
type: object
|
||||
TagtypesGettableTag:
|
||||
properties:
|
||||
key:
|
||||
@@ -14441,6 +14992,197 @@ paths:
|
||||
summary: Get stats
|
||||
tags:
|
||||
- stats
|
||||
/api/v1/subscriptions:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint gets the organization's subscription along with its
|
||||
usage and billing details.
|
||||
operationId: GetSubscription
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SubscriptiontypesGettableSubscriptionUsage'
|
||||
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:
|
||||
- subscription:read
|
||||
- tokenizer:
|
||||
- subscription:read
|
||||
summary: Get the subscription.
|
||||
tags:
|
||||
- subscriptions
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint creates a subscription for the organization.
|
||||
operationId: CreateSubscription
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SubscriptiontypesPostableSubscription'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SubscriptiontypesGettableSubscription'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: Created
|
||||
"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
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- subscription:create
|
||||
- tokenizer:
|
||||
- subscription:create
|
||||
summary: Create a subscription.
|
||||
tags:
|
||||
- subscriptions
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint updates the organization's subscription.
|
||||
operationId: UpdateSubscription
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SubscriptiontypesPostableSubscription'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SubscriptiontypesGettableSubscription'
|
||||
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:
|
||||
- subscription:list
|
||||
- subscription:update
|
||||
- tokenizer:
|
||||
- subscription:list
|
||||
- subscription:update
|
||||
summary: Update the subscription.
|
||||
tags:
|
||||
- subscriptions
|
||||
/api/v1/testChannel:
|
||||
post:
|
||||
deprecated: true
|
||||
@@ -19008,6 +19750,69 @@ paths:
|
||||
summary: Get metrics treemap
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/notification_channels:
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint creates a notification channel
|
||||
operationId: CreateNotificationChannel
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AlertmanagertypesPostableNotificationChannel'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AlertmanagertypesGettableNotificationChannel'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: Created
|
||||
"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
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- notification-channel:create
|
||||
- tokenizer:
|
||||
- notification-channel:create
|
||||
summary: Create notification channel
|
||||
tags:
|
||||
- channels
|
||||
/api/v2/orgs/me:
|
||||
get:
|
||||
deprecated: false
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
package httplicensing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type licensingAPI struct {
|
||||
licensing licensing.Licensing
|
||||
}
|
||||
|
||||
func NewLicensingAPI(licensing licensing.Licensing) licensing.API {
|
||||
return &licensingAPI{licensing: licensing}
|
||||
}
|
||||
|
||||
func (api *licensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
|
||||
return
|
||||
}
|
||||
|
||||
req := new(licensetypes.PostableSubscription)
|
||||
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
gettableSubscription, err := api.licensing.Checkout(ctx, orgID, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusCreated, gettableSubscription)
|
||||
}
|
||||
|
||||
func (api *licensingAPI) Portal(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
|
||||
return
|
||||
}
|
||||
|
||||
req := new(licensetypes.PostableSubscription)
|
||||
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
gettableSubscription, err := api.licensing.Portal(ctx, orgID, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusCreated, gettableSubscription)
|
||||
}
|
||||
@@ -2,12 +2,9 @@ package httplicensing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -228,47 +225,6 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) Checkout(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) {
|
||||
activeLicense, err := provider.GetActive(ctx, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := json.Marshal(postableSubscription)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal checkout payload")
|
||||
}
|
||||
|
||||
response, err := provider.zeus.GetCheckoutURL(ctx, activeLicense.Key, body)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeAlreadyExists) {
|
||||
return nil, errors.WithAdditionalf(err, "checkout has already been completed for this account. Please click 'Refresh Status' to sync your subscription")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Portal(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) {
|
||||
activeLicense, err := provider.GetActive(ctx, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := json.Marshal(postableSubscription)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal portal payload")
|
||||
}
|
||||
|
||||
response, err := provider.zeus.GetPortalURL(ctx, activeLicense.Key, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) GetFeatureFlags(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.Feature, error) {
|
||||
license, err := provider.GetActive(ctx, organizationID)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||
"github.com/SigNoz/signoz/ee/query-service/usage"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
@@ -42,7 +41,6 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.
|
||||
IntegrationsController: opts.IntegrationsController,
|
||||
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
||||
FluxInterval: opts.FluxInterval,
|
||||
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
|
||||
Signoz: signoz,
|
||||
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
|
||||
}, config)
|
||||
@@ -72,10 +70,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
// base overrides
|
||||
router.HandleFunc("/api/v1/version", am.OpenAccess(ah.getVersion)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.LicensingAPI.Checkout)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost)
|
||||
|
||||
// v4
|
||||
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
|
||||
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||
)
|
||||
|
||||
type DayWiseBreakdown struct {
|
||||
Type string `json:"type"`
|
||||
Breakdown []DayWiseData `json:"breakdown"`
|
||||
}
|
||||
|
||||
type DayWiseData struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Count float64 `json:"count"`
|
||||
Size float64 `json:"size"`
|
||||
UnitPrice float64 `json:"unitPrice"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Total float64 `json:"total"`
|
||||
}
|
||||
|
||||
type tierBreakdown struct {
|
||||
UnitPrice float64 `json:"unitPrice"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
TierStart int64 `json:"tierStart"`
|
||||
TierEnd int64 `json:"tierEnd"`
|
||||
TierCost float64 `json:"tierCost"`
|
||||
}
|
||||
|
||||
type usageResponse struct {
|
||||
Type string `json:"type"`
|
||||
Unit string `json:"unit"`
|
||||
Tiers []tierBreakdown `json:"tiers"`
|
||||
DayWiseBreakdown DayWiseBreakdown `json:"dayWiseBreakdown"`
|
||||
}
|
||||
|
||||
type details struct {
|
||||
Total float64 `json:"total"`
|
||||
Breakdown []usageResponse `json:"breakdown"`
|
||||
BaseFee float64 `json:"baseFee"`
|
||||
BillTotal float64 `json:"billTotal"`
|
||||
}
|
||||
|
||||
type billingData struct {
|
||||
BillingPeriodStart int64 `json:"billingPeriodStart"`
|
||||
BillingPeriodEnd int64 `json:"billingPeriodEnd"`
|
||||
Details details `json:"details"`
|
||||
Discount float64 `json:"discount"`
|
||||
SubscriptionStatus string `json:"subscriptionStatus"`
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
|
||||
licenseKey := r.URL.Query().Get("licenseKey")
|
||||
|
||||
if licenseKey == "" {
|
||||
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := ah.Signoz.Zeus.GetMeters(r.Context(), licenseKey)
|
||||
if err != nil {
|
||||
RespondError(w, model.InternalError(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
var billing billingData
|
||||
if err := json.Unmarshal(data, &billing); err != nil {
|
||||
RespondError(w, model.InternalError(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
ah.Respond(w, billing)
|
||||
}
|
||||
@@ -184,7 +184,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
apiHandler.RegisterIntegrationRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||
apiHandler.RegisterQueryRangeV4Routes(r, am)
|
||||
apiHandler.RegisterWebSocketPaths(r, am)
|
||||
apiHandler.RegisterMessagingQueuesRoutes(r, am)
|
||||
apiHandler.RegisterThirdPartyApiRoutes(r, am)
|
||||
apiHandler.RegisterTraceFunnelsRoutes(r, am)
|
||||
@@ -197,7 +196,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control", "X-SIGNOZ-QUERY-ID", "Sec-WebSocket-Protocol"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control"},
|
||||
})
|
||||
|
||||
handler := c.Handler(r)
|
||||
|
||||
95
ee/subscription/httpsubscription/provider.go
Normal file
95
ee/subscription/httpsubscription/provider.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package httpsubscription
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/subscription"
|
||||
"github.com/SigNoz/signoz/pkg/types/subscriptiontypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const upstreamTimeout = 10 * time.Second
|
||||
|
||||
type provider struct {
|
||||
zeus zeus.Zeus
|
||||
licensing licensing.Licensing
|
||||
}
|
||||
|
||||
func New(zeus zeus.Zeus, licensing licensing.Licensing) subscription.Subscription {
|
||||
return &provider{
|
||||
zeus: zeus,
|
||||
licensing: licensing,
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) Create(ctx context.Context, organizationID valuer.UUID, postableSubscription *subscriptiontypes.PostableSubscription) (*subscriptiontypes.GettableSubscription, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, upstreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
license, err := provider.licensing.GetActive(ctx, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := json.Marshal(postableSubscription)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal subscription payload")
|
||||
}
|
||||
|
||||
response, err := provider.zeus.GetCheckoutURL(ctx, license.Key, body)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeAlreadyExists) {
|
||||
return nil, errors.WithAdditionalf(err, "checkout has already been completed for this account. Please click 'Refresh Status' to sync your subscription")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &subscriptiontypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Update(ctx context.Context, organizationID valuer.UUID, postableSubscription *subscriptiontypes.PostableSubscription) (*subscriptiontypes.GettableSubscription, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, upstreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
license, err := provider.licensing.GetActive(ctx, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := json.Marshal(postableSubscription)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal subscription payload")
|
||||
}
|
||||
|
||||
response, err := provider.zeus.GetPortalURL(ctx, license.Key, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &subscriptiontypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Get(ctx context.Context, organizationID valuer.UUID) (*subscriptiontypes.GettableSubscriptionUsage, error) {
|
||||
license, err := provider.licensing.GetActive(ctx, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := provider.zeus.GetMeters(ctx, license.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usage, err := subscriptiontypes.NewGettableSubscriptionUsage(data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeus.ErrCodeResponseMalformed, "failed to unmarshal subscription usage")
|
||||
}
|
||||
|
||||
return usage, nil
|
||||
}
|
||||
@@ -323,9 +323,10 @@
|
||||
"name": "react",
|
||||
"importNames": [
|
||||
"createContext",
|
||||
"useContext"
|
||||
"useContext",
|
||||
"useSyncExternalStore"
|
||||
],
|
||||
"message": "[State mgmt] React Context is deprecated. Migrate shared state to Zustand."
|
||||
"message": "[State mgmt] React Context and hand-rolled external stores are deprecated. Migrate shared state to Zustand."
|
||||
},
|
||||
{
|
||||
"name": "immer",
|
||||
|
||||
@@ -169,12 +169,12 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
// Check for workspace blocked (trial expired)
|
||||
if (!isFetchingActiveLicense && isCloudPlatform && trialInfo?.workSpaceBlock) {
|
||||
const isRouteEnabledForWorkspaceBlockedState =
|
||||
isAdmin &&
|
||||
(pathname === ROUTES.SETTINGS ||
|
||||
pathname === ROUTES.ORG_SETTINGS ||
|
||||
pathname === ROUTES.MEMBERS_SETTINGS ||
|
||||
pathname === ROUTES.BILLING ||
|
||||
pathname === ROUTES.MY_SETTINGS);
|
||||
pathname === ROUTES.SETTINGS ||
|
||||
pathname === ROUTES.BILLING ||
|
||||
(isAdmin &&
|
||||
(pathname === ROUTES.ORG_SETTINGS ||
|
||||
pathname === ROUTES.MEMBERS_SETTINGS ||
|
||||
pathname === ROUTES.MY_SETTINGS));
|
||||
|
||||
if (
|
||||
pathname !== ROUTES.WORKSPACE_LOCKED &&
|
||||
|
||||
@@ -739,7 +739,7 @@ describe('PrivateRoute', () => {
|
||||
assertStaysOnRoute(ROUTES.MY_SETTINGS);
|
||||
});
|
||||
|
||||
it('should redirect VIEWER to workspace locked even when trying to access settings', async () => {
|
||||
it('should allow VIEWER to access /settings when workspace is blocked', () => {
|
||||
renderPrivateRoute({
|
||||
initialRoute: ROUTES.SETTINGS,
|
||||
appContext: {
|
||||
@@ -752,10 +752,10 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
await assertRedirectsTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.SETTINGS);
|
||||
});
|
||||
|
||||
it('should redirect VIEWER to workspace locked when trying to access billing', async () => {
|
||||
it('should allow VIEWER to access /settings/billing when workspace is blocked', () => {
|
||||
renderPrivateRoute({
|
||||
initialRoute: ROUTES.BILLING,
|
||||
appContext: {
|
||||
@@ -768,7 +768,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
await assertRedirectsTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.BILLING);
|
||||
});
|
||||
|
||||
it('should redirect VIEWER to workspace locked when trying to access org-settings', async () => {
|
||||
@@ -819,7 +819,7 @@ describe('PrivateRoute', () => {
|
||||
await assertRedirectsTo(ROUTES.WORKSPACE_LOCKED);
|
||||
});
|
||||
|
||||
it('should redirect EDITOR to workspace locked when trying to access settings', async () => {
|
||||
it('should allow EDITOR to access /settings when workspace is blocked', () => {
|
||||
renderPrivateRoute({
|
||||
initialRoute: ROUTES.SETTINGS,
|
||||
appContext: {
|
||||
@@ -832,7 +832,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
await assertRedirectsTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.SETTINGS);
|
||||
});
|
||||
|
||||
it('should not redirect when already on workspace locked page', () => {
|
||||
@@ -1626,6 +1626,7 @@ describe('PrivateRoute', () => {
|
||||
path: ROUTES.WORKSPACE_ACCESS_RESTRICTED,
|
||||
deniedRoles: DENIED_ROLES,
|
||||
},
|
||||
BILLING: { path: ROUTES.BILLING, deniedRoles: DENIED_ROLES },
|
||||
};
|
||||
|
||||
const authzRouteRolePairs: [string, string, ROLES][] = Object.entries(
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import axios from 'api';
|
||||
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;
|
||||
details: {
|
||||
total: number;
|
||||
baseFee: number;
|
||||
breakdown: BreakdownEntry[];
|
||||
billTotal: number;
|
||||
};
|
||||
discount: number;
|
||||
subscriptionStatus?: string;
|
||||
}
|
||||
|
||||
const getUsage = async (
|
||||
licenseKey: string,
|
||||
): Promise<SuccessResponse<UsageResponsePayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`/billing?licenseKey=${licenseKey}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getUsage;
|
||||
@@ -19,8 +19,10 @@ import type {
|
||||
|
||||
import type {
|
||||
AlertmanagertypesPostableChannelDTO,
|
||||
AlertmanagertypesPostableNotificationChannelDTO,
|
||||
AlertmanagertypesReceiverDTO,
|
||||
CreateChannel201,
|
||||
CreateNotificationChannel201,
|
||||
DeleteChannelByIDPathParameters,
|
||||
GetChannelByID200,
|
||||
GetChannelByIDPathParameters,
|
||||
@@ -647,3 +649,87 @@ export const useTestChannelDeprecated = <
|
||||
> => {
|
||||
return useMutation(getTestChannelDeprecatedMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint creates a notification channel
|
||||
* @summary Create notification channel
|
||||
*/
|
||||
export const createNotificationChannel = (
|
||||
alertmanagertypesPostableNotificationChannelDTO?: BodyType<AlertmanagertypesPostableNotificationChannelDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateNotificationChannel201>({
|
||||
url: `/api/v2/notification_channels`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: alertmanagertypesPostableNotificationChannelDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateNotificationChannelMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createNotificationChannel>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesPostableNotificationChannelDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createNotificationChannel>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesPostableNotificationChannelDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createNotificationChannel'];
|
||||
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 createNotificationChannel>>,
|
||||
{ data?: BodyType<AlertmanagertypesPostableNotificationChannelDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createNotificationChannel(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateNotificationChannelMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createNotificationChannel>>
|
||||
>;
|
||||
export type CreateNotificationChannelMutationBody =
|
||||
| BodyType<AlertmanagertypesPostableNotificationChannelDTO>
|
||||
| undefined;
|
||||
export type CreateNotificationChannelMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create notification channel
|
||||
*/
|
||||
export const useCreateNotificationChannel = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createNotificationChannel>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesPostableNotificationChannelDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createNotificationChannel>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesPostableNotificationChannelDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateNotificationChannelMutationOptions(options));
|
||||
};
|
||||
|
||||
@@ -37,6 +37,476 @@ export interface AlertmanagertypesChannelDTO {
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export enum AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelSlackConfigDTOKind {
|
||||
slack = 'slack',
|
||||
}
|
||||
export interface AlertmanagertypesChannelSlackConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
apiUrl: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
channel?: string;
|
||||
/**
|
||||
* @type boolean,null
|
||||
*/
|
||||
sendResolved?: boolean | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
text?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelSlackConfigDTO {
|
||||
/**
|
||||
* @enum slack
|
||||
* @type string
|
||||
*/
|
||||
kind: AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelSlackConfigDTOKind;
|
||||
spec: AlertmanagertypesChannelSlackConfigDTO;
|
||||
}
|
||||
|
||||
export enum AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelEmailConfigDTOKind {
|
||||
email = 'email',
|
||||
}
|
||||
export type AlertmanagertypesChannelEmailConfigDTOHeaders = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
export interface AlertmanagertypesChannelEmailConfigDTO {
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
headers?: AlertmanagertypesChannelEmailConfigDTOHeaders;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
html?: string;
|
||||
/**
|
||||
* @type boolean,null
|
||||
*/
|
||||
sendResolved?: boolean | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
to: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelEmailConfigDTO {
|
||||
/**
|
||||
* @enum email
|
||||
* @type string
|
||||
*/
|
||||
kind: AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelEmailConfigDTOKind;
|
||||
spec: AlertmanagertypesChannelEmailConfigDTO;
|
||||
}
|
||||
|
||||
export enum AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelWebhookConfigDTOKind {
|
||||
webhook = 'webhook',
|
||||
}
|
||||
export interface AlertmanagertypesChannelWebhookConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
bearerToken?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
password?: string;
|
||||
/**
|
||||
* @type boolean,null
|
||||
*/
|
||||
sendResolved?: boolean | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelWebhookConfigDTO {
|
||||
/**
|
||||
* @enum webhook
|
||||
* @type string
|
||||
*/
|
||||
kind: AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelWebhookConfigDTOKind;
|
||||
spec: AlertmanagertypesChannelWebhookConfigDTO;
|
||||
}
|
||||
|
||||
export enum AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelPagerdutyConfigDTOKind {
|
||||
pagerduty = 'pagerduty',
|
||||
}
|
||||
export type AlertmanagertypesChannelPagerdutyConfigDTODetails = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
export interface AlertmanagertypesChannelPagerdutyConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
class?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
client?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
clientUrl?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
component?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
details?: AlertmanagertypesChannelPagerdutyConfigDTODetails;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
group?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
routingKey: string;
|
||||
/**
|
||||
* @type boolean,null
|
||||
*/
|
||||
sendResolved?: boolean | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
severity?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
source?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelPagerdutyConfigDTO {
|
||||
/**
|
||||
* @enum pagerduty
|
||||
* @type string
|
||||
*/
|
||||
kind: AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelPagerdutyConfigDTOKind;
|
||||
spec: AlertmanagertypesChannelPagerdutyConfigDTO;
|
||||
}
|
||||
|
||||
export enum AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelOpsgenieConfigDTOKind {
|
||||
opsgenie = 'opsgenie',
|
||||
}
|
||||
export type AlertmanagertypesChannelOpsgenieConfigDTODetails = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
export interface AlertmanagertypesChannelOpsgenieConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
apiKey: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
apiUrl?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
details?: AlertmanagertypesChannelOpsgenieConfigDTODetails;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
message?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
priority?: string;
|
||||
/**
|
||||
* @type boolean,null
|
||||
*/
|
||||
sendResolved?: boolean | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelOpsgenieConfigDTO {
|
||||
/**
|
||||
* @enum opsgenie
|
||||
* @type string
|
||||
*/
|
||||
kind: AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelOpsgenieConfigDTOKind;
|
||||
spec: AlertmanagertypesChannelOpsgenieConfigDTO;
|
||||
}
|
||||
|
||||
export enum AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelMSTeamsConfigDTOKind {
|
||||
msteams = 'msteams',
|
||||
}
|
||||
export interface AlertmanagertypesChannelMSTeamsConfigDTO {
|
||||
/**
|
||||
* @type boolean,null
|
||||
*/
|
||||
sendResolved?: boolean | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
text?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
webhookUrl: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelMSTeamsConfigDTO {
|
||||
/**
|
||||
* @enum msteams
|
||||
* @type string
|
||||
*/
|
||||
kind: AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelMSTeamsConfigDTOKind;
|
||||
spec: AlertmanagertypesChannelMSTeamsConfigDTO;
|
||||
}
|
||||
|
||||
export enum AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelGoogleChatConfigDTOKind {
|
||||
googlechat = 'googlechat',
|
||||
}
|
||||
export interface AlertmanagertypesChannelGoogleChatConfigDTO {
|
||||
/**
|
||||
* @type boolean,null
|
||||
*/
|
||||
sendResolved?: boolean | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
text?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
webhookUrl: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelGoogleChatConfigDTO {
|
||||
/**
|
||||
* @enum googlechat
|
||||
* @type string
|
||||
*/
|
||||
kind: AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelGoogleChatConfigDTOKind;
|
||||
spec: AlertmanagertypesChannelGoogleChatConfigDTO;
|
||||
}
|
||||
|
||||
export enum AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJiraConfigDTOKind {
|
||||
jira = 'jira',
|
||||
}
|
||||
export type AlertmanagertypesChannelJiraConfigDTOCustomFields = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export interface AlertmanagertypesChannelJiraConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
apiToken: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
customFields?: AlertmanagertypesChannelJiraConfigDTOCustomFields;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
email: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
issueType: string;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
labels?: string[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
priority?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
project: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
reopenDuration?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
reopenTransition?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
resolveTransition?: string;
|
||||
/**
|
||||
* @type boolean,null
|
||||
*/
|
||||
sendResolved?: boolean | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
site: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
summary?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
wontFixResolution?: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJiraConfigDTO {
|
||||
/**
|
||||
* @enum jira
|
||||
* @type string
|
||||
*/
|
||||
kind: AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJiraConfigDTOKind;
|
||||
spec: AlertmanagertypesChannelJiraConfigDTO;
|
||||
}
|
||||
|
||||
export enum AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJSMOpsConfigDTOKind {
|
||||
jsmops = 'jsmops',
|
||||
}
|
||||
export interface AlertmanagertypesChannelJSMOpsConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
apiKey: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
message?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
priority?: string;
|
||||
/**
|
||||
* @type boolean,null
|
||||
*/
|
||||
sendResolved?: boolean | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
tags?: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJSMOpsConfigDTO {
|
||||
/**
|
||||
* @enum jsmops
|
||||
* @type string
|
||||
*/
|
||||
kind: AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJSMOpsConfigDTOKind;
|
||||
spec: AlertmanagertypesChannelJSMOpsConfigDTO;
|
||||
}
|
||||
|
||||
export enum AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelIncidentIOConfigDTOKind {
|
||||
incidentio = 'incidentio',
|
||||
}
|
||||
export type AlertmanagertypesChannelIncidentIOConfigDTOMetadata = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
export interface AlertmanagertypesChannelIncidentIOConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
metadata?: AlertmanagertypesChannelIncidentIOConfigDTOMetadata;
|
||||
/**
|
||||
* @type boolean,null
|
||||
*/
|
||||
sendResolved?: boolean | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
token: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelIncidentIOConfigDTO {
|
||||
/**
|
||||
* @enum incidentio
|
||||
* @type string
|
||||
*/
|
||||
kind: AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelIncidentIOConfigDTOKind;
|
||||
spec: AlertmanagertypesChannelIncidentIOConfigDTO;
|
||||
}
|
||||
|
||||
export type AlertmanagertypesChannelConfigDTO =
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelSlackConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelEmailConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelWebhookConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelPagerdutyConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelOpsgenieConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelMSTeamsConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelGoogleChatConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJiraConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelJSMOpsConfigDTO
|
||||
| AlertmanagertypesChannelConfigVariantGithubComSigNozSignozPkgTypesAlertmanagertypesChannelIncidentIOConfigDTO;
|
||||
|
||||
export enum AlertmanagertypesChannelKindDTO {
|
||||
slack = 'slack',
|
||||
email = 'email',
|
||||
webhook = 'webhook',
|
||||
pagerduty = 'pagerduty',
|
||||
opsgenie = 'opsgenie',
|
||||
msteams = 'msteams',
|
||||
googlechat = 'googlechat',
|
||||
jira = 'jira',
|
||||
jsmops = 'jsmops',
|
||||
incidentio = 'incidentio',
|
||||
}
|
||||
export interface ModelLabelSetDTO {
|
||||
[key: string]: string;
|
||||
}
|
||||
@@ -88,6 +558,32 @@ export enum AlertmanagertypesExpressionKindDTO {
|
||||
rule = 'rule',
|
||||
policy = 'policy',
|
||||
}
|
||||
export interface AlertmanagertypesGettableNotificationChannelDTO {
|
||||
config: AlertmanagertypesChannelConfigDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
displayName: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesGettableRoutePolicyDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
@@ -1748,6 +2244,22 @@ export type AlertmanagertypesPostableChannelDTO = unknown & {
|
||||
wechat_configs?: ConfigWechatConfigDTO[];
|
||||
};
|
||||
|
||||
export interface AlertmanagertypesPostableNotificationChannelDTO {
|
||||
config: AlertmanagertypesChannelConfigDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
generateName?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesPostablePlannedMaintenanceDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
@@ -10006,15 +10518,31 @@ export interface SpantypesGettableFlamegraphTraceDTO {
|
||||
startTimestampMillis: number;
|
||||
}
|
||||
|
||||
export enum SpantypesSpanMapperOriginDTO {
|
||||
user = 'user',
|
||||
system = 'system',
|
||||
}
|
||||
export interface SpantypesSpanMapperGroupConditionKeyDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled: boolean;
|
||||
origin?: SpantypesSpanMapperOriginDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type SpantypesSpanMapperGroupConditionDTOAnyOf = {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
attributes: string[] | null;
|
||||
attributes: SpantypesSpanMapperGroupConditionKeyDTO[] | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
resource: string[] | null;
|
||||
resource: SpantypesSpanMapperGroupConditionKeyDTO[] | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -10050,6 +10578,7 @@ export interface SpantypesSpanMapperGroupDTO {
|
||||
* @type string
|
||||
*/
|
||||
orgId: string;
|
||||
origin: SpantypesSpanMapperOriginDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -10059,6 +10588,10 @@ export interface SpantypesSpanMapperGroupDTO {
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface SpantypesGettableSpanMapperGroupsDTO {
|
||||
@@ -10116,11 +10649,16 @@ export enum SpantypesSpanMapperOperationDTO {
|
||||
}
|
||||
export interface SpantypesSpanMapperSourceDTO {
|
||||
context: SpantypesFieldContextDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
key: string;
|
||||
operation: SpantypesSpanMapperOperationDTO;
|
||||
origin?: SpantypesSpanMapperOriginDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -10162,6 +10700,7 @@ export interface SpantypesSpanMapperDTO {
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
origin: SpantypesSpanMapperOriginDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -10522,6 +11061,153 @@ export interface SpantypesUpdatableSpanMapperGroupDTO {
|
||||
name?: string | null;
|
||||
}
|
||||
|
||||
export interface SubscriptiontypesGettableSubscriptionDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
redirectURL: string;
|
||||
}
|
||||
|
||||
export interface SubscriptiontypesSubscriptionUsageDayWiseDataDTO {
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
count?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
quantity?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
size?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
timestamp?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
total?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
unitPrice?: number;
|
||||
}
|
||||
|
||||
export interface SubscriptiontypesSubscriptionUsageDayWiseBreakdownDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
breakdown?: SubscriptiontypesSubscriptionUsageDayWiseDataDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface SubscriptiontypesSubscriptionUsageTierDTO {
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
quantity?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
tierCost?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
tierEnd?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
tierStart?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
unitPrice?: number;
|
||||
}
|
||||
|
||||
export interface SubscriptiontypesSubscriptionUsageBreakdownDTO {
|
||||
dayWiseBreakdown?: SubscriptiontypesSubscriptionUsageDayWiseBreakdownDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
tiers?: SubscriptiontypesSubscriptionUsageTierDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export interface SubscriptiontypesSubscriptionUsageDetailsDTO {
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
baseFee?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
billTotal?: number;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
breakdown?: SubscriptiontypesSubscriptionUsageBreakdownDTO[] | null;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
total?: number;
|
||||
}
|
||||
|
||||
export interface SubscriptiontypesGettableSubscriptionUsageDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
billingPeriodEnd?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
billingPeriodStart?: number;
|
||||
details?: SubscriptiontypesSubscriptionUsageDetailsDTO;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
discount?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
subscriptionStatus?: string;
|
||||
}
|
||||
|
||||
export interface SubscriptiontypesPostableSubscriptionDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type TelemetrytypesGettableFieldKeysDTOKeysAnyOf = {
|
||||
[key: string]: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
};
|
||||
@@ -11740,6 +12426,30 @@ export type GetStats200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetSubscription200 = {
|
||||
data: SubscriptiontypesGettableSubscriptionUsageDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateSubscription201 = {
|
||||
data: SubscriptiontypesGettableSubscriptionDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateSubscription200 = {
|
||||
data: SubscriptiontypesGettableSubscriptionDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetTraceAggregationsPathParameters = {
|
||||
traceID: string;
|
||||
};
|
||||
@@ -12479,6 +13189,14 @@ export type GetMetricsTreemap200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateNotificationChannel201 = {
|
||||
data: AlertmanagertypesGettableNotificationChannelDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetMyOrganization200 = {
|
||||
data: TypesOrganizationDTO;
|
||||
/**
|
||||
|
||||
280
frontend/src/api/generated/services/subscriptions/index.ts
Normal file
280
frontend/src/api/generated/services/subscriptions/index.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
CreateSubscription201,
|
||||
GetSubscription200,
|
||||
RenderErrorResponseDTO,
|
||||
SubscriptiontypesPostableSubscriptionDTO,
|
||||
UpdateSubscription200,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* This endpoint gets the organization's subscription along with its usage and billing details.
|
||||
* @summary Get the subscription.
|
||||
*/
|
||||
export const getSubscription = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<GetSubscription200>({
|
||||
url: `/api/v1/subscriptions`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetSubscriptionQueryKey = () => {
|
||||
return [`/api/v1/subscriptions`] as const;
|
||||
};
|
||||
|
||||
export const getGetSubscriptionQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getSubscription>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSubscription>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetSubscriptionQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getSubscription>>> = ({
|
||||
signal,
|
||||
}) => getSubscription(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSubscription>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetSubscriptionQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getSubscription>>
|
||||
>;
|
||||
export type GetSubscriptionQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get the subscription.
|
||||
*/
|
||||
|
||||
export function useGetSubscription<
|
||||
TData = Awaited<ReturnType<typeof getSubscription>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSubscription>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetSubscriptionQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the subscription.
|
||||
*/
|
||||
export const invalidateGetSubscription = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetSubscriptionQueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint creates a subscription for the organization.
|
||||
* @summary Create a subscription.
|
||||
*/
|
||||
export const createSubscription = (
|
||||
subscriptiontypesPostableSubscriptionDTO?: BodyType<SubscriptiontypesPostableSubscriptionDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateSubscription201>({
|
||||
url: `/api/v1/subscriptions`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: subscriptiontypesPostableSubscriptionDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateSubscriptionMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createSubscription>>,
|
||||
TError,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createSubscription>>,
|
||||
TError,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createSubscription'];
|
||||
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 createSubscription>>,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createSubscription(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateSubscriptionMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createSubscription>>
|
||||
>;
|
||||
export type CreateSubscriptionMutationBody =
|
||||
| BodyType<SubscriptiontypesPostableSubscriptionDTO>
|
||||
| undefined;
|
||||
export type CreateSubscriptionMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create a subscription.
|
||||
*/
|
||||
export const useCreateSubscription = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createSubscription>>,
|
||||
TError,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createSubscription>>,
|
||||
TError,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateSubscriptionMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint updates the organization's subscription.
|
||||
* @summary Update the subscription.
|
||||
*/
|
||||
export const updateSubscription = (
|
||||
subscriptiontypesPostableSubscriptionDTO?: BodyType<SubscriptiontypesPostableSubscriptionDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<UpdateSubscription200>({
|
||||
url: `/api/v1/subscriptions`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: subscriptiontypesPostableSubscriptionDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateSubscriptionMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSubscription>>,
|
||||
TError,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSubscription>>,
|
||||
TError,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateSubscription'];
|
||||
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 updateSubscription>>,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return updateSubscription(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateSubscriptionMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateSubscription>>
|
||||
>;
|
||||
export type UpdateSubscriptionMutationBody =
|
||||
| BodyType<SubscriptiontypesPostableSubscriptionDTO>
|
||||
| undefined;
|
||||
export type UpdateSubscriptionMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update the subscription.
|
||||
*/
|
||||
export const useUpdateSubscription = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSubscription>>,
|
||||
TError,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateSubscription>>,
|
||||
TError,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateSubscriptionMutationOptions(options));
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/quickFilters/getCustomFilters';
|
||||
|
||||
const getCustomFilters = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
const { signal } = props;
|
||||
try {
|
||||
const response = await axios.get(`/orgs/me/filters/${signal}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getCustomFilters;
|
||||
@@ -1,13 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { AxiosError } from 'axios';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { UpdateCustomFiltersProps } from 'types/api/quickFilters/updateCustomFilters';
|
||||
|
||||
const updateCustomFiltersAPI = async (
|
||||
props: UpdateCustomFiltersProps,
|
||||
): Promise<SuccessResponse<void> | AxiosError> =>
|
||||
axios.put(`/orgs/me/filters`, {
|
||||
...props.data,
|
||||
});
|
||||
|
||||
export default updateCustomFiltersAPI;
|
||||
@@ -1,28 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import {
|
||||
CheckoutRequestPayloadProps,
|
||||
CheckoutSuccessPayloadProps,
|
||||
PayloadProps,
|
||||
} from 'types/api/billing/checkout';
|
||||
|
||||
const updateCreditCardApi = async (
|
||||
props: CheckoutRequestPayloadProps,
|
||||
): Promise<SuccessResponseV2<CheckoutSuccessPayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>('/checkout', {
|
||||
url: props.url,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default updateCreditCardApi;
|
||||
@@ -1,28 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import {
|
||||
CheckoutRequestPayloadProps,
|
||||
CheckoutSuccessPayloadProps,
|
||||
PayloadProps,
|
||||
} from 'types/api/billing/checkout';
|
||||
|
||||
const manageCreditCardApi = async (
|
||||
props: CheckoutRequestPayloadProps,
|
||||
): Promise<SuccessResponseV2<CheckoutSuccessPayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>('/portal', {
|
||||
url: props.url,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default manageCreditCardApi;
|
||||
@@ -2,8 +2,10 @@ import { cloneDeep, isEmpty } from 'lodash-es';
|
||||
import { SuccessResponse, Warning } from 'types/api';
|
||||
import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
|
||||
import {
|
||||
BuilderQuery,
|
||||
DistributionData,
|
||||
MetricRangePayloadV5,
|
||||
QueryEnvelope,
|
||||
QueryRangeRequestV5,
|
||||
RawData,
|
||||
ScalarData,
|
||||
@@ -11,6 +13,11 @@ import {
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
|
||||
const isBuilderQueryEnvelope = (
|
||||
envelope: QueryEnvelope,
|
||||
): envelope is QueryEnvelope & { spec: BuilderQuery } =>
|
||||
envelope.type === 'builder_query' || envelope.type === 'builder_ai_query';
|
||||
|
||||
function getColName(
|
||||
col: ScalarData['columns'][number],
|
||||
legendMap: Record<string, string>,
|
||||
@@ -409,21 +416,19 @@ export function convertV5ResponseToLegacy(
|
||||
const v5Data = payload?.data;
|
||||
|
||||
const aggregationPerQuery =
|
||||
params?.compositeQuery?.queries
|
||||
?.filter((query) => query.type === 'builder_query')
|
||||
.reduce(
|
||||
(acc, query) => {
|
||||
if (
|
||||
query.type === 'builder_query' &&
|
||||
'aggregations' in query.spec &&
|
||||
query.spec.name
|
||||
) {
|
||||
acc[query.spec.name] = query.spec.aggregations;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
) || {};
|
||||
params?.compositeQuery?.queries?.filter(isBuilderQueryEnvelope).reduce(
|
||||
(acc, query) => {
|
||||
if (
|
||||
isBuilderQueryEnvelope(query) &&
|
||||
'aggregations' in query.spec &&
|
||||
query.spec.name
|
||||
) {
|
||||
acc[query.spec.name] = query.spec.aggregations;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
) || {};
|
||||
|
||||
// clickhouse_sql queries have no aggregation metadata; their value columns
|
||||
// are named/keyed by the real SQL alias the response carries (see getColId).
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
QueryBuilderFormula as V5QueryBuilderFormula,
|
||||
QueryEnvelope,
|
||||
QueryRangePayloadV5,
|
||||
RequestType,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
@@ -935,3 +936,41 @@ describe('convertBuilderQueriesToV5 having normalization', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertBuilderQueriesToV5 builder query type', () => {
|
||||
const buildEnvelope = (
|
||||
builderQueryType: IBuilderQuery['builderQueryType'],
|
||||
requestType: RequestType,
|
||||
): QueryEnvelope => {
|
||||
const [envelope] = convertBuilderQueriesToV5(
|
||||
{
|
||||
A: {
|
||||
dataSource: DataSource.TRACES,
|
||||
queryName: 'A',
|
||||
builderQueryType,
|
||||
} as unknown as IBuilderQuery,
|
||||
},
|
||||
requestType,
|
||||
);
|
||||
return envelope;
|
||||
};
|
||||
|
||||
it.each<[RequestType]>([
|
||||
['trace'],
|
||||
['raw'],
|
||||
['time_series'],
|
||||
['scalar'],
|
||||
['distribution'],
|
||||
])('sends builder_ai_query for the %s request type', (requestType) => {
|
||||
expect(buildEnvelope('builder_ai_query', requestType).type).toBe(
|
||||
'builder_ai_query',
|
||||
);
|
||||
});
|
||||
|
||||
it.each<[string, IBuilderQuery['builderQueryType']]>([
|
||||
['an unmarked query', undefined],
|
||||
['an explicitly generic query', 'builder_query'],
|
||||
])('sends builder_query for %s', (_label, builderQueryType) => {
|
||||
expect(buildEnvelope(builderQueryType, 'trace').type).toBe('builder_query');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { DynamicVariableSuggestion } from 'providers/Dashboard/store/dynamicVariableSuggestions';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
@@ -364,7 +365,7 @@ export function convertBuilderQueriesToV5(
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'builder_query' as QueryType,
|
||||
type: queryData.builderQueryType ?? 'builder_query',
|
||||
spec,
|
||||
};
|
||||
},
|
||||
@@ -545,20 +546,22 @@ function reduceQueriesToObject(queryArray: any[]): {
|
||||
/**
|
||||
* Prepares V5 query range payload from GetQueryResultsProps
|
||||
*/
|
||||
export const prepareQueryRangePayloadV5 = ({
|
||||
query,
|
||||
globalSelectedInterval,
|
||||
graphType,
|
||||
selectedTime,
|
||||
tableParams,
|
||||
variables = {},
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
formatForWeb,
|
||||
originalGraphType,
|
||||
fillGaps,
|
||||
dynamicVariables,
|
||||
}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => {
|
||||
export const prepareQueryRangePayloadV5 = (
|
||||
{
|
||||
query,
|
||||
globalSelectedInterval,
|
||||
graphType,
|
||||
selectedTime,
|
||||
tableParams,
|
||||
variables = {},
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
formatForWeb,
|
||||
originalGraphType,
|
||||
fillGaps,
|
||||
}: GetQueryResultsProps,
|
||||
dynamicVariables: DynamicVariableSuggestion[] = [],
|
||||
): PrepareQueryRangePayloadV5Result => {
|
||||
let legendMap: Record<string, string> = {};
|
||||
const requestType = mapPanelTypeToRequestType(graphType);
|
||||
let queries: QueryEnvelope[] = [];
|
||||
@@ -671,9 +674,9 @@ export const prepareQueryRangePayloadV5 = ({
|
||||
(acc, [key, value]) => {
|
||||
acc[key] = {
|
||||
value,
|
||||
type: dynamicVariables
|
||||
?.find((v) => v.name === key)
|
||||
?.type?.toLowerCase() as VariableType,
|
||||
type: dynamicVariables.some((v) => v.name === key)
|
||||
? ('dynamic' as VariableType)
|
||||
: undefined,
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
|
||||
@@ -4,11 +4,12 @@ import { useLocation } from 'react-router-dom';
|
||||
import { Button, Modal } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import updateCreditCardApi from 'api/v1/checkout/create';
|
||||
import { createSubscription } from 'api/generated/services/subscriptions';
|
||||
import type { CreateSubscription201 } from 'api/generated/services/sigNoz.schemas';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import { SubscriptionCreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/subscription.permissions';
|
||||
import { CreditCard, MessageSquareText, X } from '@signozhq/icons';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import APIError from 'types/api/error';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
|
||||
@@ -18,9 +19,7 @@ export default function ChatSupportGateway(): JSX.Element {
|
||||
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const handleBillingOnSuccess = (
|
||||
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
|
||||
): void => {
|
||||
const handleBillingOnSuccess = (data: CreateSubscription201): void => {
|
||||
if (data?.data?.redirectURL) {
|
||||
const newTab = document.createElement('a');
|
||||
newTab.href = data.data.redirectURL;
|
||||
@@ -38,7 +37,7 @@ export default function ChatSupportGateway(): JSX.Element {
|
||||
};
|
||||
|
||||
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
|
||||
updateCreditCardApi,
|
||||
createSubscription,
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
handleBillingOnSuccess(data);
|
||||
@@ -94,18 +93,23 @@ export default function ChatSupportGateway(): JSX.Element {
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
<AuthZTooltip
|
||||
key="submit"
|
||||
type="primary"
|
||||
icon={<CreditCard size={16} />}
|
||||
size="middle"
|
||||
loading={isLoadingBilling}
|
||||
disabled={isLoadingBilling}
|
||||
onClick={handleAddCreditCard}
|
||||
className="add-credit-card-btn"
|
||||
checks={[SubscriptionCreatePermission]}
|
||||
withPortal={false}
|
||||
>
|
||||
Add Credit Card
|
||||
</Button>,
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CreditCard size={16} />}
|
||||
size="middle"
|
||||
loading={isLoadingBilling}
|
||||
disabled={isLoadingBilling}
|
||||
onClick={handleAddCreditCard}
|
||||
className="add-credit-card-btn"
|
||||
>
|
||||
Add Credit Card
|
||||
</Button>
|
||||
</AuthZTooltip>,
|
||||
]}
|
||||
>
|
||||
<Typography.Text className="add-credit-card-text">
|
||||
|
||||
@@ -4,16 +4,17 @@ import { useLocation } from 'react-router-dom';
|
||||
import { Button, Modal, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import updateCreditCardApi from 'api/v1/checkout/create';
|
||||
import { createSubscription } from 'api/generated/services/subscriptions';
|
||||
import type { CreateSubscription201 } from 'api/generated/services/sigNoz.schemas';
|
||||
import cx from 'classnames';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import { SubscriptionCreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/subscription.permissions';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { CircleHelp, CreditCard, X } from '@signozhq/icons';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import APIError from 'types/api/error';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
|
||||
@@ -118,9 +119,7 @@ function LaunchChatSupport({
|
||||
}
|
||||
};
|
||||
|
||||
const handleBillingOnSuccess = (
|
||||
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
|
||||
): void => {
|
||||
const handleBillingOnSuccess = (data: CreateSubscription201): void => {
|
||||
if (data?.data?.redirectURL) {
|
||||
const newTab = document.createElement('a');
|
||||
newTab.href = data.data.redirectURL;
|
||||
@@ -138,7 +137,7 @@ function LaunchChatSupport({
|
||||
};
|
||||
|
||||
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
|
||||
updateCreditCardApi,
|
||||
createSubscription,
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
handleBillingOnSuccess(data);
|
||||
@@ -193,18 +192,23 @@ function LaunchChatSupport({
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
<AuthZTooltip
|
||||
key="submit"
|
||||
type="primary"
|
||||
icon={<CreditCard size={16} />}
|
||||
size="middle"
|
||||
loading={isLoadingBilling}
|
||||
disabled={isLoadingBilling}
|
||||
onClick={handleAddCreditCard}
|
||||
className="add-credit-card-btn"
|
||||
checks={[SubscriptionCreatePermission]}
|
||||
withPortal={false}
|
||||
>
|
||||
Add Credit Card
|
||||
</Button>,
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CreditCard size={16} />}
|
||||
size="middle"
|
||||
loading={isLoadingBilling}
|
||||
disabled={isLoadingBilling}
|
||||
onClick={handleAddCreditCard}
|
||||
className="add-credit-card-btn"
|
||||
>
|
||||
Add Credit Card
|
||||
</Button>
|
||||
</AuthZTooltip>,
|
||||
]}
|
||||
>
|
||||
<Typography.Text className="add-credit-card-text">
|
||||
|
||||
@@ -16,8 +16,6 @@ import { githubLight } from '@uiw/codemirror-theme-github';
|
||||
import CodeMirror, { EditorView, keymap, Prec } from '@uiw/react-codemirror';
|
||||
import { Button, Card, Collapse, Popover, Tooltip } from 'antd';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
negationQueryOperatorSuggestions,
|
||||
@@ -27,7 +25,7 @@ import {
|
||||
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE,
|
||||
queryOperatorSuggestions,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
|
||||
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { debounce, isNull } from 'lodash-es';
|
||||
@@ -54,6 +52,12 @@ import {
|
||||
SUGGESTION_FETCH_DEBOUNCE_MS,
|
||||
SUGGESTIONS_SECTION,
|
||||
} from './constants';
|
||||
import {
|
||||
fetchFieldKeysForQuery,
|
||||
fetchFieldValuesForQuery,
|
||||
SuggestedFieldKey,
|
||||
SuggestedFieldKeysByName,
|
||||
} from './fieldSuggestions';
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
dedupeOptionsByLabel,
|
||||
@@ -258,16 +262,11 @@ function QuerySearch({
|
||||
const lastValueRef = useRef<string>('');
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
|
||||
const dashboardDynamicVariables = useDashboardVariablesByType(
|
||||
'DYNAMIC',
|
||||
'values',
|
||||
);
|
||||
const dashboardDynamicVariables = useDynamicVariableSuggestions();
|
||||
|
||||
// Add back the generateOptions function and useEffect
|
||||
const generateOptions = (keys: {
|
||||
[key: string]: QueryKeyDataSuggestionsProps[];
|
||||
}): any[] =>
|
||||
Object.values(keys).flatMap((items: QueryKeyDataSuggestionsProps[]) =>
|
||||
const generateOptions = (keys: SuggestedFieldKeysByName): any[] =>
|
||||
Object.values(keys).flatMap((items: SuggestedFieldKey[]) =>
|
||||
items.map(({ name, fieldDataType, fieldContext }) => ({
|
||||
label: name,
|
||||
type: fieldDataType === 'string' ? 'keyword' : fieldDataType,
|
||||
@@ -320,8 +319,9 @@ function QuerySearch({
|
||||
|
||||
lastFetchedKeyRef.current = searchText || '';
|
||||
|
||||
const response = await getKeySuggestions({
|
||||
signal: dataSource,
|
||||
const response = await fetchFieldKeysForQuery({
|
||||
builderQueryType: queryData.builderQueryType,
|
||||
dataSource,
|
||||
searchText: searchText || '',
|
||||
metricName: debouncedMetricName ?? undefined,
|
||||
signalSource: signalSource as 'meter' | '',
|
||||
@@ -363,6 +363,7 @@ function QuerySearch({
|
||||
hardcodedAttributeKeys,
|
||||
showFilterSuggestionsWithoutMetric,
|
||||
metricNamespace,
|
||||
queryData.builderQueryType,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -496,10 +497,11 @@ function QuerySearch({
|
||||
try {
|
||||
const values = valueSuggestionsOverride
|
||||
? await valueSuggestionsOverride(key, sanitizedSearchText)
|
||||
: await getValueSuggestions({
|
||||
: await fetchFieldValuesForQuery({
|
||||
builderQueryType: queryData.builderQueryType,
|
||||
dataSource,
|
||||
key,
|
||||
searchText: sanitizedSearchText,
|
||||
signal: dataSource,
|
||||
signalSource: signalSource as 'meter' | '',
|
||||
metricName: debouncedMetricName ?? undefined,
|
||||
}).then((response) => {
|
||||
@@ -604,6 +606,7 @@ function QuerySearch({
|
||||
signalSource,
|
||||
toggleSuggestions,
|
||||
valueSuggestionsOverride,
|
||||
queryData.builderQueryType,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1188,8 +1191,8 @@ function QuerySearch({
|
||||
);
|
||||
|
||||
// Add dynamic variables suggestions for the current key
|
||||
const variableName = dashboardDynamicVariables?.find(
|
||||
(variable) => variable?.dynamicVariablesAttribute === keyName,
|
||||
const variableName = dashboardDynamicVariables.find(
|
||||
(variable) => variable.attribute === keyName,
|
||||
)?.name;
|
||||
|
||||
if (variableName) {
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import {
|
||||
getAIObservabilityFieldsKeys,
|
||||
getAIObservabilityFieldsValues,
|
||||
} from 'api/generated/services/ai-observability';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
fetchFieldKeysForQuery,
|
||||
fetchFieldValuesForQuery,
|
||||
} from '../fieldSuggestions';
|
||||
|
||||
jest.mock('api/generated/services/ai-observability', () => ({
|
||||
getAIObservabilityFieldsKeys: jest.fn(),
|
||||
getAIObservabilityFieldsValues: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
|
||||
getKeySuggestions: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getValueSuggestion', () => ({
|
||||
getValueSuggestions: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockedAIKeys = getAIObservabilityFieldsKeys as jest.MockedFunction<
|
||||
typeof getAIObservabilityFieldsKeys
|
||||
>;
|
||||
const mockedGenericKeys = getKeySuggestions as jest.MockedFunction<
|
||||
typeof getKeySuggestions
|
||||
>;
|
||||
const mockedAIValues = getAIObservabilityFieldsValues as jest.MockedFunction<
|
||||
typeof getAIObservabilityFieldsValues
|
||||
>;
|
||||
const mockedGenericValues = getValueSuggestions as jest.MockedFunction<
|
||||
typeof getValueSuggestions
|
||||
>;
|
||||
|
||||
const aiValuesResponse = (
|
||||
values: { stringValues?: string[]; numberValues?: number[] } | null,
|
||||
complete = true,
|
||||
): Awaited<ReturnType<typeof getAIObservabilityFieldsValues>> =>
|
||||
({
|
||||
status: 'success',
|
||||
data: { complete, values },
|
||||
}) as Awaited<ReturnType<typeof getAIObservabilityFieldsValues>>;
|
||||
|
||||
describe('fetchFieldKeysForQuery', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('reads the ai_observability endpoint for a builder_ai_query', async () => {
|
||||
mockedAIKeys.mockResolvedValue({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
keys: { llm_call_count: [{ name: 'llm_call_count' }] },
|
||||
},
|
||||
} as Awaited<ReturnType<typeof getAIObservabilityFieldsKeys>>);
|
||||
|
||||
const keys = await fetchFieldKeysForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
searchText: 'llm',
|
||||
});
|
||||
|
||||
expect(mockedAIKeys).toHaveBeenCalledWith({ searchText: 'llm' });
|
||||
expect(mockedGenericKeys).not.toHaveBeenCalled();
|
||||
expect(keys.data.data).toStrictEqual({
|
||||
complete: true,
|
||||
keys: { llm_call_count: [{ name: 'llm_call_count' }] },
|
||||
});
|
||||
});
|
||||
|
||||
it.each<[string, 'builder_query' | undefined]>([
|
||||
['an unmarked query', undefined],
|
||||
['an explicitly generic query', 'builder_query'],
|
||||
])('reads the generic endpoint for %s', async (_label, builderQueryType) => {
|
||||
mockedGenericKeys.mockResolvedValue({
|
||||
data: { status: 'success', data: { complete: true, keys: {} } },
|
||||
} as Awaited<ReturnType<typeof getKeySuggestions>>);
|
||||
|
||||
await fetchFieldKeysForQuery({
|
||||
builderQueryType,
|
||||
dataSource: DataSource.TRACES,
|
||||
searchText: 'svc',
|
||||
});
|
||||
|
||||
expect(mockedAIKeys).not.toHaveBeenCalled();
|
||||
expect(mockedGenericKeys).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ signal: DataSource.TRACES, searchText: 'svc' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes a null ai_observability keys payload to an empty map', async () => {
|
||||
mockedAIKeys.mockResolvedValue({
|
||||
status: 'success',
|
||||
data: { complete: false, keys: null },
|
||||
} as Awaited<ReturnType<typeof getAIObservabilityFieldsKeys>>);
|
||||
|
||||
const response = await fetchFieldKeysForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
searchText: '',
|
||||
});
|
||||
|
||||
expect(response.data.data).toStrictEqual({ complete: false, keys: {} });
|
||||
});
|
||||
|
||||
it('passes the generic response through untouched', async () => {
|
||||
const genericResponse = {
|
||||
data: { status: 'success', data: { complete: true, keys: {} } },
|
||||
} as unknown as Awaited<ReturnType<typeof getKeySuggestions>>;
|
||||
mockedGenericKeys.mockResolvedValue(genericResponse);
|
||||
|
||||
await expect(
|
||||
fetchFieldKeysForQuery({
|
||||
builderQueryType: 'builder_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
searchText: '',
|
||||
}),
|
||||
).resolves.toBe(genericResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchFieldValuesForQuery', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('reads the ai_observability endpoint for a builder_ai_query', async () => {
|
||||
mockedAIValues.mockResolvedValue(
|
||||
aiValuesResponse({ stringValues: ['gpt-4o'], numberValues: [] }),
|
||||
);
|
||||
|
||||
const response = await fetchFieldValuesForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
key: 'gen_ai.request.model',
|
||||
searchText: 'gpt',
|
||||
});
|
||||
|
||||
expect(mockedGenericValues).not.toHaveBeenCalled();
|
||||
expect(response).toStrictEqual({
|
||||
data: {
|
||||
data: {
|
||||
complete: true,
|
||||
values: { stringValues: ['gpt-4o'], numberValues: [] },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards the key as the name the endpoint expects', async () => {
|
||||
mockedAIValues.mockResolvedValue(aiValuesResponse({}));
|
||||
|
||||
await fetchFieldValuesForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
key: 'total_tokens',
|
||||
searchText: '',
|
||||
});
|
||||
|
||||
expect(mockedAIValues).toHaveBeenCalledWith({
|
||||
name: 'total_tokens',
|
||||
searchText: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('wraps the ai_observability payload in the envelope the call site unwraps', async () => {
|
||||
mockedAIValues.mockResolvedValue(aiValuesResponse(null, false));
|
||||
|
||||
await expect(
|
||||
fetchFieldValuesForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
key: 'llm_call_count',
|
||||
searchText: '',
|
||||
}),
|
||||
).resolves.toStrictEqual({
|
||||
data: { data: { complete: false, values: null } },
|
||||
});
|
||||
});
|
||||
|
||||
it.each<[string, 'builder_query' | undefined]>([
|
||||
['an unmarked query', undefined],
|
||||
['an explicitly generic query', 'builder_query'],
|
||||
])('reads the generic endpoint for %s', async (_label, builderQueryType) => {
|
||||
const genericResponse = {
|
||||
data: {
|
||||
data: { complete: false, values: { stringValues: ['frontend'] } },
|
||||
},
|
||||
} as unknown as Awaited<ReturnType<typeof getValueSuggestions>>;
|
||||
mockedGenericValues.mockResolvedValue(genericResponse);
|
||||
|
||||
const response = await fetchFieldValuesForQuery({
|
||||
builderQueryType,
|
||||
dataSource: DataSource.TRACES,
|
||||
key: 'service.name',
|
||||
searchText: 'front',
|
||||
});
|
||||
|
||||
expect(mockedAIValues).not.toHaveBeenCalled();
|
||||
expect(mockedGenericValues).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
signal: DataSource.TRACES,
|
||||
key: 'service.name',
|
||||
searchText: 'front',
|
||||
}),
|
||||
);
|
||||
expect(response).toBe(genericResponse);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
getAIObservabilityFieldsKeys,
|
||||
getAIObservabilityFieldsValues,
|
||||
} from 'api/generated/services/ai-observability';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export interface SuggestedFieldKey {
|
||||
name: string;
|
||||
fieldContext?: string;
|
||||
fieldDataType?: string;
|
||||
}
|
||||
|
||||
export type SuggestedFieldKeysByName = Record<string, SuggestedFieldKey[]>;
|
||||
|
||||
export interface SuggestedFieldKeysPayload {
|
||||
complete: boolean;
|
||||
keys: SuggestedFieldKeysByName;
|
||||
}
|
||||
|
||||
export interface SuggestedFieldKeysResponse {
|
||||
data: { data?: SuggestedFieldKeysPayload };
|
||||
}
|
||||
|
||||
export interface SuggestedFieldValuesPayload {
|
||||
complete?: boolean;
|
||||
values?: {
|
||||
stringValues?: string[] | null;
|
||||
numberValues?: number[] | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface SuggestedFieldValuesResponse {
|
||||
data: { data?: SuggestedFieldValuesPayload };
|
||||
}
|
||||
|
||||
interface FetchFieldKeysParams {
|
||||
builderQueryType: IBuilderQuery['builderQueryType'];
|
||||
dataSource: DataSource;
|
||||
searchText: string;
|
||||
metricName?: string;
|
||||
signalSource?: 'meter' | '';
|
||||
metricNamespace?: string;
|
||||
}
|
||||
|
||||
interface FetchFieldValuesParams {
|
||||
builderQueryType: IBuilderQuery['builderQueryType'];
|
||||
dataSource: DataSource;
|
||||
key: string;
|
||||
searchText: string;
|
||||
metricName?: string;
|
||||
signalSource?: 'meter' | '';
|
||||
}
|
||||
|
||||
export const fetchFieldKeysForQuery = async ({
|
||||
builderQueryType,
|
||||
dataSource,
|
||||
searchText,
|
||||
metricName,
|
||||
signalSource,
|
||||
metricNamespace,
|
||||
}: FetchFieldKeysParams): Promise<SuggestedFieldKeysResponse> => {
|
||||
if (builderQueryType === 'builder_ai_query') {
|
||||
const response = await getAIObservabilityFieldsKeys({ searchText });
|
||||
|
||||
return {
|
||||
data: {
|
||||
data: response.data
|
||||
? { complete: response.data.complete, keys: response.data.keys ?? {} }
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return getKeySuggestions({
|
||||
signal: dataSource,
|
||||
searchText,
|
||||
metricName,
|
||||
signalSource,
|
||||
metricNamespace,
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchFieldValuesForQuery = async ({
|
||||
builderQueryType,
|
||||
dataSource,
|
||||
key,
|
||||
searchText,
|
||||
metricName,
|
||||
signalSource,
|
||||
}: FetchFieldValuesParams): Promise<SuggestedFieldValuesResponse> => {
|
||||
if (builderQueryType === 'builder_ai_query') {
|
||||
const response = await getAIObservabilityFieldsValues({
|
||||
name: key,
|
||||
searchText,
|
||||
});
|
||||
|
||||
return { data: { data: response.data } };
|
||||
}
|
||||
|
||||
// getValueSuggestions' declared response type does not match what the endpoint returns.
|
||||
return getValueSuggestions({
|
||||
signal: dataSource,
|
||||
key,
|
||||
searchText,
|
||||
signalSource,
|
||||
metricName,
|
||||
}) as unknown as Promise<SuggestedFieldValuesResponse>;
|
||||
};
|
||||
@@ -54,7 +54,7 @@ export const QueryV2 = forwardRef(function QueryV2(
|
||||
const { cloneQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const showFunctions = query?.functions?.length > 0;
|
||||
const { dataSource } = query;
|
||||
const { dataSource, builderQueryType } = query;
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
@@ -94,8 +94,9 @@ export const QueryV2 = forwardRef(function QueryV2(
|
||||
);
|
||||
|
||||
const showSpanScopeSelector = useMemo(
|
||||
() => dataSource === DataSource.TRACES,
|
||||
[dataSource],
|
||||
() =>
|
||||
dataSource === DataSource.TRACES && builderQueryType !== 'builder_ai_query',
|
||||
[dataSource, builderQueryType],
|
||||
);
|
||||
|
||||
const showInlineQuerySearch = useMemo(() => {
|
||||
|
||||
@@ -525,6 +525,34 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Canonical name for a comparison's operator, limited to the equality and
|
||||
* membership forms. Every other shape (LIKE, BETWEEN, EXISTS, CONTAINS, REGEXP,
|
||||
* the ordering operators) returns undefined, so an operator-restricted removal
|
||||
* leaves it in place.
|
||||
*
|
||||
* The ANTLR4 runtime returns null for an absent token or rule despite the
|
||||
* non-nullable TypeScript signatures.
|
||||
*/
|
||||
const getComparisonOperator = (ctx: ComparisonContext): string | undefined => {
|
||||
if ((ctx.inClause() as unknown) !== null) {
|
||||
return 'in';
|
||||
}
|
||||
if ((ctx.notInClause() as unknown) !== null) {
|
||||
return 'not in';
|
||||
}
|
||||
if ((ctx.EQUALS() as unknown) !== null) {
|
||||
return '=';
|
||||
}
|
||||
if (
|
||||
(ctx.NOT_EQUALS() as unknown) !== null ||
|
||||
(ctx.NEQ() as unknown) !== null
|
||||
) {
|
||||
return '!=';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes clauses for specified keys from a filter query expression.
|
||||
*
|
||||
@@ -542,12 +570,16 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
||||
* - `true`: removes only the first clause whose value contains any `$`.
|
||||
* - `string` (e.g. `"$service.name"`): removes only the clause whose value exactly
|
||||
* matches that string — preferred when the specific variable reference is known.
|
||||
* @param operatorsToRemove - When given, restricts removal to clauses whose operator
|
||||
* is in this set (`=`, `!=`, `in`, `not in`); every other clause on the key is kept.
|
||||
* Omit to remove a matching key's clauses whatever their operator.
|
||||
* @returns The rewritten expression, or an empty string if all clauses were removed.
|
||||
*/
|
||||
export const removeKeysFromExpression = (
|
||||
expression: string,
|
||||
keysToRemove: string[],
|
||||
removeOnlyVariableExpressions: string | boolean = false,
|
||||
operatorsToRemove?: string[],
|
||||
): string => {
|
||||
if (!keysToRemove || keysToRemove.length === 0) {
|
||||
return expression;
|
||||
@@ -557,6 +589,9 @@ export const removeKeysFromExpression = (
|
||||
}
|
||||
|
||||
const keysSet = new Set(keysToRemove.map((k) => k.trim().toLowerCase()));
|
||||
const operatorsSet = operatorsToRemove
|
||||
? new Set(operatorsToRemove.map((op) => op.trim().toLowerCase()))
|
||||
: null;
|
||||
// Tracks keys for which a variable expression has already been removed.
|
||||
// Having multiple $-value clauses for the same key is invalid; we remove at most one.
|
||||
const removedVariableKeys = new Set<string>();
|
||||
@@ -658,6 +693,13 @@ export const removeKeysFromExpression = (
|
||||
return src(ctx);
|
||||
}
|
||||
|
||||
if (operatorsSet) {
|
||||
const operator = getComparisonOperator(ctx);
|
||||
if (!operator || !operatorsSet.has(operator)) {
|
||||
return src(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
if (removeOnlyVariableExpressions) {
|
||||
// Scope the value check to value nodes only — not the full comparison text —
|
||||
// so a key that contains '$' does not trigger removal when the value is a
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
import {
|
||||
convertFiltersToExpression,
|
||||
convertFiltersToExpressionWithExistingQuery,
|
||||
} from 'components/QueryBuilderV2/utils';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import {
|
||||
Query,
|
||||
TagFilter,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
applyCheckboxToggle,
|
||||
clearFilterFromQuery,
|
||||
deriveCheckboxState,
|
||||
getNotInOperator,
|
||||
} from './checkboxFilterQuery';
|
||||
import { CheckedState } from '../../types';
|
||||
import { SectionType } from './v2/itemRules';
|
||||
|
||||
const KEY = 'service.name';
|
||||
|
||||
/**
|
||||
* Mini test framework
|
||||
* -------------------
|
||||
* `filters.items` is the source of truth the checkbox algebra mutates.
|
||||
* `filter.expression` is the derived value the backend actually reads, and it is
|
||||
* authoritatively rebuilt from the items on every URL round trip
|
||||
* (`useGetCompositeQueryParam` -> `convertFiltersToExpressionWithExistingQuery`).
|
||||
* That rebuild is additive, so `applyCheckboxToggle` re-derives its own clauses
|
||||
* into the expression itself: otherwise the round trip resurrects a clause the
|
||||
* toggle removed, or appends a duplicate of one it replaced.
|
||||
*
|
||||
* So a case does not assert the intermediate expression the toggle emits. It
|
||||
* asserts the pair that has to stay consistent:
|
||||
* - `items` : exact structured clauses after the toggle
|
||||
* - `expression` : the expression AFTER the round trip, which is what ships
|
||||
*
|
||||
* `runToggle` runs the real reducer, then feeds its output through the real
|
||||
* converter to get the shipped expression.
|
||||
*/
|
||||
|
||||
type SimpleItem = {
|
||||
key: string;
|
||||
op: string;
|
||||
value: TagFilterItem['value'];
|
||||
};
|
||||
|
||||
function toTagItem(item: SimpleItem, idx: number): TagFilterItem {
|
||||
return {
|
||||
id: `id-${idx}`,
|
||||
key: { key: item.key, type: 'tag' } as TagFilterItem['key'],
|
||||
op: item.op,
|
||||
value: item.value,
|
||||
};
|
||||
}
|
||||
|
||||
// Serialises items into an expression (via the app's own converter) so a case's
|
||||
// starting state is self-consistent (items and expression agree), the way it
|
||||
// would be in the app after a prior round trip.
|
||||
const serializeItems = (items: SimpleItem[]): string =>
|
||||
convertFiltersToExpression({ items: items.map(toTagItem), op: 'AND' })
|
||||
.expression;
|
||||
|
||||
function buildQuery(items: SimpleItem[], expression: string): Query {
|
||||
return {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
filters: { items: items.map(toTagItem), op: 'AND' },
|
||||
filter: { expression },
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as Query;
|
||||
}
|
||||
|
||||
// Simulates the URL round trip: rebuild the shipped expression from the items,
|
||||
// reconciled against whatever expression the toggle left behind. Trimmed to
|
||||
// absorb a converter quirk that leaves a trailing space when it widens an
|
||||
// operator in place (e.g. `=` -> `IN`).
|
||||
function roundTripExpression(
|
||||
items: TagFilterItem[],
|
||||
emittedExpression: string,
|
||||
): string {
|
||||
const filters: TagFilter = { items, op: 'AND' };
|
||||
const { filter } = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
emittedExpression,
|
||||
);
|
||||
return (filter?.expression ?? '').trim();
|
||||
}
|
||||
|
||||
interface ToggleAction {
|
||||
value: string;
|
||||
checked: boolean;
|
||||
isOnlyOrAllClicked?: boolean;
|
||||
previousState?: CheckedState;
|
||||
sectionType?: SectionType;
|
||||
source?: QuickFiltersSource;
|
||||
attributeValues?: string[];
|
||||
}
|
||||
|
||||
interface ToggleCase {
|
||||
name: string;
|
||||
initial?: { items?: SimpleItem[]; expression?: string };
|
||||
action: ToggleAction;
|
||||
expected: { items: SimpleItem[]; expression: string };
|
||||
}
|
||||
|
||||
function runToggle(c: ToggleCase): { items: SimpleItem[]; expression: string } {
|
||||
const initialItems = c.initial?.items ?? [];
|
||||
const initialExpression =
|
||||
c.initial?.expression ?? serializeItems(initialItems);
|
||||
|
||||
const result = applyCheckboxToggle({
|
||||
currentQuery: buildQuery(initialItems, initialExpression),
|
||||
activeQueryIndex: 0,
|
||||
filter: { attributeKey: { key: KEY, type: 'tag' } } as never,
|
||||
source: c.action.source ?? QuickFiltersSource.LOGS_EXPLORER,
|
||||
attributeValues: c.action.attributeValues ?? ['a', 'b', 'c'],
|
||||
value: c.action.value,
|
||||
checked: c.action.checked,
|
||||
isOnlyOrAllClicked: c.action.isOnlyOrAllClicked ?? false,
|
||||
previousState: c.action.previousState,
|
||||
sectionType: c.action.sectionType,
|
||||
});
|
||||
|
||||
const active = result.builder.queryData[0];
|
||||
const items = active?.filters?.items ?? [];
|
||||
return {
|
||||
items: items.map((item) => ({
|
||||
key: item.key?.key ?? '',
|
||||
op: item.op,
|
||||
value: item.value,
|
||||
})),
|
||||
expression: roundTripExpression(items, active?.filter?.expression ?? ''),
|
||||
};
|
||||
}
|
||||
|
||||
// Flat list. Every row asserts both the structured items and the shipped
|
||||
// (round-tripped) expression, which must stay in sync.
|
||||
const TOGGLE_CASES: ToggleCase[] = [
|
||||
{
|
||||
name: 'no clause, checked -> IN',
|
||||
action: { value: 'a', checked: true },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: 'a' }],
|
||||
expression: `service.name in ['a']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'no clause, unchecked -> NOT IN',
|
||||
action: { value: 'a', checked: false },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: 'a' }],
|
||||
expression: `service.name not in ['a']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'no clause, unchecked on infra -> not in',
|
||||
action: {
|
||||
value: 'a',
|
||||
checked: false,
|
||||
source: QuickFiltersSource.INFRA_MONITORING,
|
||||
},
|
||||
// `nin` is what the source asks for, but re-deriving the expression
|
||||
// normalises it. Nothing observes the difference: both infra pages send
|
||||
// `filter.expression` and never `filters.items`.
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: 'a' }],
|
||||
expression: `service.name not in ['a']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'IN, check another value -> appended',
|
||||
initial: { items: [{ key: KEY, op: 'in', value: ['a'] }] },
|
||||
action: { value: 'b', checked: true },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: ['a', 'b'] }],
|
||||
expression: `service.name in ['a', 'b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'IN, check when value is scalar -> promoted to array',
|
||||
initial: { items: [{ key: KEY, op: 'in', value: 'a' }] },
|
||||
action: { value: 'b', checked: true },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: ['a', 'b'] }],
|
||||
expression: `service.name in ['a', 'b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'IN, uncheck one of many -> filtered out',
|
||||
initial: { items: [{ key: KEY, op: 'in', value: ['a', 'b'] }] },
|
||||
action: { value: 'a', checked: false },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: ['b'] }],
|
||||
expression: `service.name in ['b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'IN, uncheck last value in array -> clause gone',
|
||||
initial: { items: [{ key: KEY, op: 'in', value: ['a'] }] },
|
||||
action: { value: 'a', checked: false },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: 'IN, uncheck scalar value -> clause gone',
|
||||
initial: { items: [{ key: KEY, op: 'in', value: 'a' }] },
|
||||
action: { value: 'a', checked: false },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: 'IN, uncheck in RELATED section -> replaced by NOT IN for that value',
|
||||
initial: { items: [{ key: KEY, op: 'in', value: ['a', 'b'] }] },
|
||||
action: { value: 'a', checked: false, sectionType: SectionType.RELATED },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: 'a' }],
|
||||
expression: `service.name not in ['a']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NOT IN, was unchecked then checked -> replaced by IN for that value',
|
||||
initial: { items: [{ key: KEY, op: 'not in', value: ['a'] }] },
|
||||
action: { value: 'b', checked: true, previousState: 'unchecked' },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: 'b' }],
|
||||
expression: `service.name in ['b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NOT IN, re-checking an excluded value clears it, not flips it to IN',
|
||||
initial: { items: [{ key: KEY, op: 'not in', value: ['a'] }] },
|
||||
action: { value: 'a', checked: true, previousState: 'unchecked' },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: 'NOT IN, re-checking one of several excluded values keeps the rest',
|
||||
initial: { items: [{ key: KEY, op: 'not in', value: ['a', 'b'] }] },
|
||||
action: { value: 'a', checked: true, previousState: 'unchecked' },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: ['b'] }],
|
||||
expression: `service.name not in ['b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NOT IN, exclude another value -> appended',
|
||||
initial: { items: [{ key: KEY, op: 'not in', value: ['a'] }] },
|
||||
action: { value: 'b', checked: false },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: ['a', 'b'] }],
|
||||
expression: `service.name not in ['a', 'b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NOT IN, exclude when scalar -> promoted to array',
|
||||
initial: { items: [{ key: KEY, op: 'not in', value: 'a' }] },
|
||||
action: { value: 'b', checked: false },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: ['a', 'b'] }],
|
||||
expression: `service.name not in ['a', 'b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NOT IN, check an excluded value -> removed from array',
|
||||
initial: { items: [{ key: KEY, op: 'not in', value: ['a', 'b'] }] },
|
||||
action: { value: 'a', checked: true },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: ['b'] }],
|
||||
expression: `service.name not in ['b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NOT IN, check last excluded value in array -> clause gone',
|
||||
initial: { items: [{ key: KEY, op: 'not in', value: ['a'] }] },
|
||||
action: { value: 'a', checked: true },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: 'NOT IN, check excluded scalar value -> clause gone',
|
||||
initial: { items: [{ key: KEY, op: 'not in', value: 'a' }] },
|
||||
action: { value: 'a', checked: true },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: '= check another value -> promoted to IN array',
|
||||
initial: { items: [{ key: KEY, op: '=', value: 'a' }] },
|
||||
action: { value: 'b', checked: true },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: ['a', 'b'] }],
|
||||
expression: `service.name in ['a', 'b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '= uncheck -> clause gone',
|
||||
initial: { items: [{ key: KEY, op: '=', value: 'a' }] },
|
||||
action: { value: 'a', checked: false },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: '!= exclude another value -> promoted to NOT IN array',
|
||||
initial: { items: [{ key: KEY, op: '!=', value: 'a' }] },
|
||||
action: { value: 'b', checked: false },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: ['a', 'b'] }],
|
||||
expression: `service.name not in ['a', 'b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '!= exclude another value on infra -> not in array',
|
||||
initial: { items: [{ key: KEY, op: '!=', value: 'a' }] },
|
||||
action: {
|
||||
value: 'b',
|
||||
checked: false,
|
||||
source: QuickFiltersSource.INFRA_MONITORING,
|
||||
},
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: ['a', 'b'] }],
|
||||
expression: `service.name not in ['a', 'b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '!= check -> clause gone',
|
||||
initial: { items: [{ key: KEY, op: '!=', value: 'a' }] },
|
||||
action: { value: 'a', checked: true },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: 'Only with no clause -> IN scalar',
|
||||
action: { value: 'a', checked: true, isOnlyOrAllClicked: true },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: 'a' }],
|
||||
expression: `service.name in ['a']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Only replaces a multi-value IN with a single value',
|
||||
initial: { items: [{ key: KEY, op: 'in', value: ['a', 'b'] }] },
|
||||
action: { value: 'a', checked: true, isOnlyOrAllClicked: true },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: 'a' }],
|
||||
expression: `service.name in ['a']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'All (clicking the sole selected value) -> clause gone',
|
||||
initial: { items: [{ key: KEY, op: 'in', value: ['a'] }] },
|
||||
action: { value: 'a', checked: true, isOnlyOrAllClicked: true },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: 'dropping the last clause keeps other keys in the expression',
|
||||
initial: {
|
||||
items: [{ key: KEY, op: 'in', value: 'a' }],
|
||||
expression: `${KEY} = 'a' AND http.method = 'GET'`,
|
||||
},
|
||||
action: { value: 'a', checked: false },
|
||||
// The seeded items omit the http.method clause the expression carries;
|
||||
// re-deriving reconciles it back, which is why items is not empty here.
|
||||
expected: {
|
||||
items: [{ key: 'http.method', op: '=', value: 'GET' }],
|
||||
expression: `http.method = 'GET'`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dropping the last clause strips the prefixed spelling too',
|
||||
initial: {
|
||||
items: [{ key: 'resource.service.name', op: 'in', value: 'a' }],
|
||||
expression: `resource.service.name = 'a'`,
|
||||
},
|
||||
action: { value: 'a', checked: false },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: 'removing the value must keep a free-form clause on the same key',
|
||||
initial: {
|
||||
items: [{ key: KEY, op: '=', value: 'a' }],
|
||||
expression: `${KEY} = 'a' AND ${KEY} CONTAINS 'keepme'`,
|
||||
},
|
||||
action: { value: 'a', checked: false },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'contains', value: 'keepme' }],
|
||||
expression: `service.name CONTAINS 'keepme'`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'a second clause on the same key must not survive an add',
|
||||
initial: {
|
||||
items: [{ key: KEY, op: 'in', value: ['a'] }],
|
||||
expression: `${KEY} IN ['a'] AND ${KEY} != 'z'`,
|
||||
},
|
||||
action: { value: 'b', checked: true },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: ['a', 'b'] }],
|
||||
expression: `service.name in ['a', 'b']`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('applyCheckboxToggle (items + shipped expression stay in sync)', () => {
|
||||
it.each(TOGGLE_CASES)('$name', (c) => {
|
||||
const got = runToggle(c);
|
||||
expect(got.items).toStrictEqual(c.expected.items);
|
||||
expect(got.expression).toBe(c.expected.expression);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNotInOperator', () => {
|
||||
it('returns short "nin" for infra monitoring', () => {
|
||||
expect(getNotInOperator(QuickFiltersSource.INFRA_MONITORING)).toBe('nin');
|
||||
});
|
||||
|
||||
it('returns long "not in" for other sources', () => {
|
||||
expect(getNotInOperator(QuickFiltersSource.LOGS_EXPLORER)).toBe('not in');
|
||||
expect(getNotInOperator(QuickFiltersSource.TRACES_EXPLORER)).toBe('not in');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deriveCheckboxState', () => {
|
||||
const attributeValues = ['a', 'b', 'c'];
|
||||
|
||||
const state = (items: TagFilterItem[] | undefined): Record<string, boolean> =>
|
||||
deriveCheckboxState({ attributeValues, filterItems: items, filterKey: KEY });
|
||||
|
||||
it('no clause for key -> everything checked', () => {
|
||||
expect(state([])).toStrictEqual({ a: true, b: true, c: true });
|
||||
expect(state(undefined)).toStrictEqual({ a: true, b: true, c: true });
|
||||
});
|
||||
|
||||
it('unrelated clause only -> everything checked', () => {
|
||||
expect(
|
||||
state([toTagItem({ key: 'other', op: 'in', value: ['a'] }, 0)]),
|
||||
).toStrictEqual({ a: true, b: true, c: true });
|
||||
});
|
||||
|
||||
it('IN [list] -> only listed values checked', () => {
|
||||
expect(
|
||||
state([toTagItem({ key: KEY, op: 'in', value: ['a', 'c'] }, 0)]),
|
||||
).toStrictEqual({ a: true, b: false, c: true });
|
||||
});
|
||||
|
||||
it('= "value" -> only that value checked', () => {
|
||||
expect(
|
||||
state([toTagItem({ key: KEY, op: '=', value: 'b' }, 0)]),
|
||||
).toStrictEqual({ a: false, b: true, c: false });
|
||||
});
|
||||
|
||||
it('NOT IN [list] -> everything except excluded checked', () => {
|
||||
expect(
|
||||
state([toTagItem({ key: KEY, op: 'not in', value: ['a'] }, 0)]),
|
||||
).toStrictEqual({ a: false, b: true, c: true });
|
||||
});
|
||||
|
||||
it('!= "value" -> everything except that value checked', () => {
|
||||
expect(
|
||||
state([toTagItem({ key: KEY, op: '!=', value: 'b' }, 0)]),
|
||||
).toStrictEqual({ a: true, b: false, c: true });
|
||||
});
|
||||
|
||||
it('matches by base key across context prefixes', () => {
|
||||
expect(
|
||||
state([
|
||||
toTagItem({ key: 'resource.service.name', op: 'in', value: ['a'] }, 0),
|
||||
]),
|
||||
).toStrictEqual({ a: true, b: false, c: false });
|
||||
});
|
||||
|
||||
it('coerces boolean / number values to string keys', () => {
|
||||
expect(
|
||||
deriveCheckboxState({
|
||||
attributeValues: ['true', '42'],
|
||||
filterItems: [toTagItem({ key: KEY, op: '=', value: true }, 0)],
|
||||
filterKey: KEY,
|
||||
}),
|
||||
).toStrictEqual({ true: true, '42': false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearFilterFromQuery', () => {
|
||||
it('removes the key from items and expression at the active index only', () => {
|
||||
const query = {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
filters: {
|
||||
items: [
|
||||
toTagItem({ key: KEY, op: 'in', value: ['a'] }, 0),
|
||||
toTagItem({ key: 'http.method', op: '=', value: 'GET' }, 1),
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
filter: { expression: `${KEY} = 'a' AND http.method = 'GET'` },
|
||||
},
|
||||
{
|
||||
filters: {
|
||||
items: [toTagItem({ key: KEY, op: 'in', value: ['a'] }, 2)],
|
||||
op: 'AND',
|
||||
},
|
||||
filter: { expression: `${KEY} = 'a'` },
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as Query;
|
||||
|
||||
const result = clearFilterFromQuery({
|
||||
currentQuery: query,
|
||||
filter: { attributeKey: { key: KEY, type: 'tag' } } as never,
|
||||
activeQueryIndex: 0,
|
||||
});
|
||||
|
||||
const active = result.builder.queryData[0];
|
||||
expect(active.filters?.items).toStrictEqual([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: 'http.method' }),
|
||||
}),
|
||||
]);
|
||||
expect(active.filter?.expression).toBe(`http.method = 'GET'`);
|
||||
|
||||
// Other queries keep both halves: stripping their expression while leaving
|
||||
// their items alone only churned a clause the round trip put straight back.
|
||||
const other = result.builder.queryData[1];
|
||||
expect(other.filters?.items).toHaveLength(1);
|
||||
expect(other.filter?.expression).toBe(`${KEY} = 'a'`);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,8 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
|
||||
import {
|
||||
convertFiltersToExpressionWithExistingQuery,
|
||||
removeKeysFromExpression,
|
||||
} from 'components/QueryBuilderV2/utils';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
@@ -10,13 +13,33 @@ import { cloneDeep, isArray } from 'lodash-es';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { isKeyMatch } from './utils';
|
||||
import { getKeySpellings, isKeyMatch } from './utils';
|
||||
import { CheckedState } from '../../types';
|
||||
import { SectionType } from './v2/itemRules';
|
||||
|
||||
export const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
|
||||
export const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'not in', 'nin'];
|
||||
|
||||
// The operators this algebra emits, and so the only ones it may rewrite out of an
|
||||
// expression. A hand-written clause on the same key (CONTAINS, EXISTS, a range) is
|
||||
// none of its business and has to survive a toggle.
|
||||
const MANAGED_OPERATORS = [OPERATORS['='], OPERATORS['!='], 'in', 'not in'];
|
||||
|
||||
/**
|
||||
* Drops this filter's own clauses for `key` from `expression`, leaving every other
|
||||
* key and any clause the checkbox does not manage untouched. Matches all context
|
||||
* prefixes, since `isKeyMatch` treats `service.name` and `resource.service.name` as
|
||||
* the same filter but expression rewrites match keys literally.
|
||||
*/
|
||||
function removeManagedClauses(expression: string, key: string): string {
|
||||
return removeKeysFromExpression(
|
||||
expression,
|
||||
getKeySpellings(key),
|
||||
false,
|
||||
MANAGED_OPERATORS,
|
||||
);
|
||||
}
|
||||
|
||||
// Sources that use backend APIs expecting short operator format (e.g., 'nin' instead of 'not in')
|
||||
const SOURCES_WITH_SHORT_OPERATORS = [QuickFiltersSource.INFRA_MONITORING];
|
||||
|
||||
@@ -102,8 +125,8 @@ export function deriveCheckboxState({
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new query with every clause for this attribute key removed, both
|
||||
* from the structured filter items and the raw filter expression.
|
||||
* Returns a new query with this filter's clauses for the attribute key removed from
|
||||
* the active query, both from the structured filter items and the raw expression.
|
||||
*/
|
||||
export function clearFilterFromQuery({
|
||||
currentQuery,
|
||||
@@ -118,24 +141,28 @@ export function clearFilterFromQuery({
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => ({
|
||||
...item,
|
||||
filter: {
|
||||
expression: removeKeysFromExpression(item.filter?.expression ?? '', [
|
||||
filter.attributeKey.key,
|
||||
]),
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items:
|
||||
idx === activeQueryIndex
|
||||
? item.filters?.items?.filter(
|
||||
(fil) => !isKeyMatch(fil.key?.key, filter.attributeKey.key),
|
||||
) || []
|
||||
: [...(item.filters?.items || [])],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
})),
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
filter: {
|
||||
expression: removeManagedClauses(
|
||||
item.filter?.expression ?? '',
|
||||
filter.attributeKey.key,
|
||||
),
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items:
|
||||
item.filters?.items?.filter(
|
||||
(fil) => !isKeyMatch(fil.key?.key, filter.attributeKey.key),
|
||||
) || [],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -194,12 +221,6 @@ export function applyCheckboxToggle({
|
||||
(q) => !isKeyMatch(q.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
|
||||
if (query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(query.filter.expression, [
|
||||
filter.attributeKey.key,
|
||||
]);
|
||||
}
|
||||
|
||||
if (isOnlyOrAll === 'Only') {
|
||||
const newFilterItem: TagFilterItem = {
|
||||
id: uuid(),
|
||||
@@ -267,12 +288,6 @@ export function applyCheckboxToggle({
|
||||
}
|
||||
return item;
|
||||
});
|
||||
if (query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
} else if (isArray(currentFilter.value)) {
|
||||
// if we are removing some value when the running operator is IN we filter.
|
||||
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
|
||||
@@ -309,9 +324,10 @@ export function applyCheckboxToggle({
|
||||
? currentFilter.value.includes(value)
|
||||
: currentFilter.value === value;
|
||||
|
||||
// When clicking unchecked "Other" item, user wants to SELECT it
|
||||
// Replace NOT IN filter with IN [value]
|
||||
if (previousState === 'unchecked' && checked) {
|
||||
// When clicking an unchecked value that is not itself excluded, the user
|
||||
// wants to SELECT it: replace the NOT IN filter with IN [value]. A value
|
||||
// that IS in the exclusion list falls through to the removal branch below.
|
||||
if (previousState === 'unchecked' && checked && !isValueInFilter) {
|
||||
const newFilter: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: getOperatorValue(OPERATORS.IN),
|
||||
@@ -324,12 +340,6 @@ export function applyCheckboxToggle({
|
||||
}
|
||||
return item;
|
||||
});
|
||||
if (query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
} else if (!checked || !isValueInFilter) {
|
||||
// Add to NOT IN when:
|
||||
// - checked=false (user explicitly unchecked to exclude)
|
||||
@@ -369,12 +379,6 @@ export function applyCheckboxToggle({
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
if (query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
@@ -384,16 +388,6 @@ export function applyCheckboxToggle({
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: currentFilter.value === value ? null : currentFilter.value,
|
||||
};
|
||||
if (newFilter.value === null && query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
@@ -456,6 +450,18 @@ export function applyCheckboxToggle({
|
||||
}
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const synced = convertFiltersToExpressionWithExistingQuery(
|
||||
query.filters ?? { items: [], op: 'AND' },
|
||||
removeManagedClauses(
|
||||
query.filter?.expression ?? '',
|
||||
filter.attributeKey.key,
|
||||
),
|
||||
);
|
||||
query.filter = synced.filter;
|
||||
query.filters = synced.filters;
|
||||
}
|
||||
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
|
||||
@@ -39,3 +39,16 @@ export function isKeyMatch(
|
||||
): boolean {
|
||||
return getKeyWithoutPrefix(itemKey) === getKeyWithoutPrefix(filterKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every spelling of a key that `isKeyMatch` treats as equal: the base name plus
|
||||
* each context-prefixed form. Expression rewrites match keys literally, so they
|
||||
* need the whole list where the items side only needs `isKeyMatch`.
|
||||
*/
|
||||
export function getKeySpellings(key: string | undefined): string[] {
|
||||
const base = getKeyWithoutPrefix(key);
|
||||
if (!base) {
|
||||
return [];
|
||||
}
|
||||
return [base, ...FIELD_CONTEXT_PREFIXES.map((prefix) => `${prefix}.${base}`)];
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export function mockFieldsValuesAPI(response: {
|
||||
relatedValues?: (string | null)[];
|
||||
stringValues?: (string | null)[];
|
||||
numberValues?: (number | null)[];
|
||||
boolValues?: (boolean | null)[];
|
||||
}): void {
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v1/fields/values', (_, res, ctx) =>
|
||||
@@ -46,6 +47,7 @@ export function mockFieldsValuesAPI(response: {
|
||||
relatedValues: response.relatedValues ?? [],
|
||||
stringValues: response.stringValues ?? [],
|
||||
numberValues: response.numberValues ?? [],
|
||||
boolValues: response.boolValues ?? [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -22,7 +22,10 @@ interface UseFieldValuesReturn {
|
||||
isFetching: boolean;
|
||||
}
|
||||
|
||||
const DATA_SOURCE_TO_SIGNAL: Record<DataSource, TelemetrytypesSignalDTO> = {
|
||||
export const DATA_SOURCE_TO_SIGNAL: Record<
|
||||
DataSource,
|
||||
TelemetrytypesSignalDTO
|
||||
> = {
|
||||
[DataSource.METRICS]: TelemetrytypesSignalDTO.metrics,
|
||||
[DataSource.TRACES]: TelemetrytypesSignalDTO.traces,
|
||||
[DataSource.LOGS]: TelemetrytypesSignalDTO.logs,
|
||||
@@ -89,8 +92,12 @@ export function useFieldValues({
|
||||
values.numberValues
|
||||
?.filter((value): value is number => value !== null && value !== undefined)
|
||||
.map((value) => value.toString()) || [];
|
||||
const boolValues =
|
||||
values.boolValues
|
||||
?.filter((value): value is boolean => value !== null && value !== undefined)
|
||||
.map((value) => value.toString()) || [];
|
||||
|
||||
return [...stringValues, ...numberValues];
|
||||
return [...stringValues, ...numberValues, ...boolValues];
|
||||
}, [data]);
|
||||
|
||||
return { relatedValues, allValues, isLoading, isFetching };
|
||||
|
||||
@@ -17,7 +17,7 @@ import { CSS } from '@dnd-kit/utilities';
|
||||
import { Button } from 'antd';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { GripVertical } from '@signozhq/icons';
|
||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
function SortableFilter({
|
||||
filter,
|
||||
@@ -25,13 +25,13 @@ function SortableFilter({
|
||||
allowDrag,
|
||||
allowRemove,
|
||||
}: {
|
||||
filter: FilterType;
|
||||
onRemove: (filter: FilterType) => void;
|
||||
filter: TelemetryFieldKey;
|
||||
onRemove: (filter: TelemetryFieldKey) => void;
|
||||
allowDrag: boolean;
|
||||
allowRemove: boolean;
|
||||
}): JSX.Element {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({ id: filter.key });
|
||||
useSortable({ id: filter.key as string });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
@@ -46,14 +46,14 @@ function SortableFilter({
|
||||
>
|
||||
<div {...attributes} {...listeners} className="drag-handle">
|
||||
{allowDrag && <GripVertical size={16} />}
|
||||
{filter.key}
|
||||
{filter.name}
|
||||
</div>
|
||||
{allowRemove && (
|
||||
<Button
|
||||
className="remove-filter-btn periscope-btn"
|
||||
size="small"
|
||||
onClick={(): void => {
|
||||
onRemove(filter as FilterType);
|
||||
onRemove(filter);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
@@ -69,8 +69,8 @@ function AddedFilters({
|
||||
setAddedFilters,
|
||||
}: {
|
||||
inputValue: string;
|
||||
addedFilters: FilterType[];
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
|
||||
addedFilters: TelemetryFieldKey[];
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
|
||||
}): JSX.Element {
|
||||
const sensors = useSensors(useSensor(PointerSensor));
|
||||
|
||||
@@ -90,12 +90,12 @@ function AddedFilters({
|
||||
const filteredAddedFilters = useMemo(
|
||||
() =>
|
||||
addedFilters.filter((filter) =>
|
||||
filter.key.toLowerCase().includes(inputValue.toLowerCase()),
|
||||
filter.name.toLowerCase().includes(inputValue.toLowerCase()),
|
||||
),
|
||||
[addedFilters, inputValue],
|
||||
);
|
||||
|
||||
const handleRemoveFilter = (filter: FilterType): void => {
|
||||
const handleRemoveFilter = (filter: TelemetryFieldKey): void => {
|
||||
setAddedFilters((prev) => prev.filter((f) => f.key !== filter.key));
|
||||
};
|
||||
|
||||
@@ -116,7 +116,7 @@ function AddedFilters({
|
||||
<div className="no-values-found">No values found</div>
|
||||
) : (
|
||||
<SortableContext
|
||||
items={addedFilters.map((f) => f.key)}
|
||||
items={addedFilters.map((f) => f.key as string)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
disabled={!allowDrag}
|
||||
>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import { useGetFieldsKeys } from 'api/generated/services/fields';
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { DATA_SOURCE_TO_SIGNAL } from 'components/QuickFilters/FilterRenderers/Checkbox/v2/useFieldValues';
|
||||
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions';
|
||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
|
||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import {
|
||||
FieldContext,
|
||||
FieldDataType,
|
||||
TelemetryFieldKey,
|
||||
} from 'types/api/v5/queryRange';
|
||||
|
||||
function OtherFiltersSkeleton(): JSX.Element {
|
||||
return (
|
||||
@@ -37,106 +37,48 @@ function OtherFilters({
|
||||
}: {
|
||||
signal: SignalType | undefined;
|
||||
inputValue: string;
|
||||
addedFilters: FilterType[];
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
|
||||
addedFilters: TelemetryFieldKey[];
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
|
||||
}): JSX.Element {
|
||||
const isLogDataSource = useMemo(
|
||||
() => SIGNAL_DATA_SOURCE_MAP[signal as SignalType] === DataSource.LOGS,
|
||||
[signal],
|
||||
);
|
||||
const isMeterDataSource = useMemo(
|
||||
() => signal && signal === SignalType.METER_EXPLORER,
|
||||
[signal],
|
||||
const isMeterDataSource = signal === SignalType.METER_EXPLORER;
|
||||
|
||||
const { data, isFetching } = useGetFieldsKeys(
|
||||
{
|
||||
searchText: inputValue,
|
||||
signal: signal
|
||||
? DATA_SOURCE_TO_SIGNAL[SIGNAL_DATA_SOURCE_MAP[signal]]
|
||||
: undefined,
|
||||
source: isMeterDataSource ? TelemetrytypesSourceDTO.meter : undefined,
|
||||
},
|
||||
{ query: { enabled: !!signal } },
|
||||
);
|
||||
|
||||
const { data: suggestionsData, isFetching: isFetchingSuggestions } =
|
||||
useGetAttributeSuggestions(
|
||||
{
|
||||
searchText: inputValue,
|
||||
dataSource: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
|
||||
filters: {} as TagFilter,
|
||||
},
|
||||
{
|
||||
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
|
||||
enabled: !!signal && isLogDataSource,
|
||||
},
|
||||
);
|
||||
const otherFilters = useMemo<TelemetryFieldKey[]>(() => {
|
||||
const rawSuggestions = Object.values(data?.data?.keys ?? {}).flat();
|
||||
// Normalize: synthesize the composite `key` once so downstream reads (dedupe,
|
||||
// add, render) can trust it.
|
||||
const suggestions: TelemetryFieldKey[] = rawSuggestions.map((attr) => ({
|
||||
name: attr.name,
|
||||
signal: attr.signal as TelemetryFieldKey['signal'],
|
||||
fieldContext: attr.fieldContext as FieldContext,
|
||||
fieldDataType: attr.fieldDataType as FieldDataType,
|
||||
key: buildCompositeKey(attr.name, attr.fieldContext, attr.fieldDataType),
|
||||
}));
|
||||
|
||||
const { data: aggregateKeysData, isFetching: isFetchingAggregateKeys } =
|
||||
useGetAggregateKeys(
|
||||
{
|
||||
searchText: inputValue,
|
||||
dataSource: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
|
||||
enabled: !!signal && !isLogDataSource && !isMeterDataSource,
|
||||
},
|
||||
const addedKeys = new Set(
|
||||
addedFilters.map((filter) =>
|
||||
buildCompositeKey(filter.name, filter.fieldContext, filter.fieldDataType),
|
||||
),
|
||||
);
|
||||
return suggestions.filter((attr) => !addedKeys.has(attr.key as string));
|
||||
}, [data, addedFilters]);
|
||||
|
||||
const { data: fieldKeysData, isLoading: isLoadingFieldKeys } =
|
||||
useGetQueryKeySuggestions(
|
||||
{
|
||||
searchText: inputValue,
|
||||
signal: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
|
||||
signalSource: 'meter',
|
||||
},
|
||||
{
|
||||
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
|
||||
enabled: !!signal && isMeterDataSource,
|
||||
},
|
||||
);
|
||||
|
||||
const otherFilters = useMemo(() => {
|
||||
let filterAttributes;
|
||||
if (isLogDataSource) {
|
||||
filterAttributes = suggestionsData?.payload?.attributes || [];
|
||||
} else if (isMeterDataSource) {
|
||||
const fieldKeys: QueryKeyDataSuggestionsProps[] = Object.values(
|
||||
fieldKeysData?.data?.data?.keys || {},
|
||||
)?.flat();
|
||||
filterAttributes = fieldKeys.map(
|
||||
(attr) =>
|
||||
({
|
||||
key: attr.name,
|
||||
dataType: attr.fieldDataType,
|
||||
type: attr.fieldContext,
|
||||
signal: attr.signal,
|
||||
}) as BaseAutocompleteData,
|
||||
);
|
||||
} else {
|
||||
filterAttributes = aggregateKeysData?.payload?.attributeKeys || [];
|
||||
}
|
||||
return filterAttributes?.filter(
|
||||
(attr) => !addedFilters.some((filter) => filter.key === attr.key),
|
||||
);
|
||||
}, [
|
||||
suggestionsData,
|
||||
aggregateKeysData,
|
||||
addedFilters,
|
||||
isLogDataSource,
|
||||
fieldKeysData,
|
||||
isMeterDataSource,
|
||||
]);
|
||||
|
||||
const handleAddFilter = (filter: FilterType): void => {
|
||||
setAddedFilters((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: filter.key,
|
||||
dataType: filter.dataType,
|
||||
type: filter.type,
|
||||
},
|
||||
]);
|
||||
const handleAddFilter = (filter: TelemetryFieldKey): void => {
|
||||
setAddedFilters((prev) => [...prev, filter]);
|
||||
};
|
||||
|
||||
const renderFilters = (): React.ReactNode => {
|
||||
const isLoading =
|
||||
isFetchingSuggestions || isFetchingAggregateKeys || isLoadingFieldKeys;
|
||||
if (isLoading) {
|
||||
if (isFetching) {
|
||||
return <OtherFiltersSkeleton />;
|
||||
}
|
||||
if (!otherFilters?.length) {
|
||||
@@ -145,11 +87,11 @@ function OtherFilters({
|
||||
|
||||
return otherFilters.map((filter) => (
|
||||
<div key={filter.key} className="qf-filter-item other-filters-item">
|
||||
<div className="qf-filter-key">{filter.key}</div>
|
||||
<div className="qf-filter-key">{filter.name}</div>
|
||||
<Button
|
||||
className="add-filter-btn periscope-btn"
|
||||
size="small"
|
||||
onClick={(): void => handleAddFilter(filter as FilterType)}
|
||||
onClick={(): void => handleAddFilter(filter)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button } from 'antd';
|
||||
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
|
||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
import { SignalType } from '../types';
|
||||
import AddedFilters from './AddedFilters';
|
||||
@@ -19,7 +18,7 @@ function QuickFiltersSettings({
|
||||
}: {
|
||||
signal: SignalType | undefined;
|
||||
setIsSettingsOpen: (isSettingsOpen: boolean) => void;
|
||||
customFilters: FilterType[];
|
||||
customFilters: TelemetryFieldKey[];
|
||||
refetchCustomFilters: () => void;
|
||||
}): JSX.Element {
|
||||
const {
|
||||
@@ -28,6 +27,7 @@ function QuickFiltersSettings({
|
||||
addedFilters,
|
||||
setAddedFilters,
|
||||
handleSaveChanges,
|
||||
hasUnsavedChanges,
|
||||
isUpdatingCustomFilters,
|
||||
inputValue,
|
||||
handleInputChange,
|
||||
@@ -39,18 +39,6 @@ function QuickFiltersSettings({
|
||||
signal,
|
||||
});
|
||||
|
||||
const hasUnsavedChanges = useMemo(
|
||||
() =>
|
||||
// check if both arrays have the same length and same order of elements
|
||||
!(
|
||||
addedFilters.length === customFilters.length &&
|
||||
addedFilters.every(
|
||||
(filter, index) => filter.key === customFilters[index].key,
|
||||
)
|
||||
),
|
||||
[addedFilters, customFilters],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="qf-header">
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useUpdateQuickFilters } from 'api/generated/services/quick-filter';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import updateCustomFiltersAPI from 'api/quickFilters/updateCustomFilters';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import {
|
||||
TelemetrytypesFieldContextDTO,
|
||||
TelemetrytypesFieldDataTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
interface UseQuickFilterSettingsProps {
|
||||
setIsSettingsOpen: (isSettingsOpen: boolean) => void;
|
||||
customFilters: FilterType[];
|
||||
customFilters: TelemetryFieldKey[];
|
||||
refetchCustomFilters: () => void;
|
||||
signal?: SignalType;
|
||||
}
|
||||
|
||||
interface UseQuickFilterSettingsReturn {
|
||||
addedFilters: FilterType[];
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
|
||||
addedFilters: TelemetryFieldKey[];
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
|
||||
handleSettingsClose: () => void;
|
||||
handleDiscardChanges: () => void;
|
||||
handleSaveChanges: () => void;
|
||||
hasUnsavedChanges: boolean;
|
||||
isUpdatingCustomFilters: boolean;
|
||||
inputValue: string;
|
||||
setInputValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
@@ -37,27 +41,43 @@ const useQuickFilterSettings = ({
|
||||
}: UseQuickFilterSettingsProps): UseQuickFilterSettingsReturn => {
|
||||
const [inputValue, setInputValue] = useState<string>('');
|
||||
const [debouncedInputValue, setDebouncedInputValue] = useState<string>('');
|
||||
const [addedFilters, setAddedFilters] = useState<FilterType[]>(customFilters);
|
||||
const normalizedCustomFilters = useMemo<TelemetryFieldKey[]>(
|
||||
() =>
|
||||
customFilters.map((filter) => ({
|
||||
...filter,
|
||||
key: buildCompositeKey(
|
||||
filter.name,
|
||||
filter.fieldContext,
|
||||
filter.fieldDataType,
|
||||
),
|
||||
})),
|
||||
[customFilters],
|
||||
);
|
||||
const [addedFilters, setAddedFilters] = useState<TelemetryFieldKey[]>(
|
||||
normalizedCustomFilters,
|
||||
);
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { mutate: updateCustomFilters, isLoading: isUpdatingCustomFilters } =
|
||||
useMutation(updateCustomFiltersAPI, {
|
||||
onSuccess: () => {
|
||||
setIsSettingsOpen(false);
|
||||
refetchCustomFilters();
|
||||
logEvent('Quick Filters Settings: changes saved', {
|
||||
addedFilters,
|
||||
});
|
||||
notifications.success({
|
||||
message: 'Quick filters updated successfully',
|
||||
placement: 'bottomRight',
|
||||
});
|
||||
},
|
||||
onError: (error: AxiosError) => {
|
||||
notifications.error({
|
||||
message: axios.isAxiosError(error) ? error.message : SOMETHING_WENT_WRONG,
|
||||
placement: 'bottomRight',
|
||||
});
|
||||
useUpdateQuickFilters({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
setIsSettingsOpen(false);
|
||||
refetchCustomFilters();
|
||||
void logEvent('Quick Filters Settings: changes saved', {
|
||||
addedFilters,
|
||||
});
|
||||
notifications.success({
|
||||
message: 'Quick filters updated successfully',
|
||||
placement: 'bottomRight',
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.error({
|
||||
message: error.message || SOMETHING_WENT_WRONG,
|
||||
placement: 'bottomRight',
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
const debouncedUpdate = useDebouncedFn((value) => {
|
||||
@@ -78,19 +98,32 @@ const useQuickFilterSettings = ({
|
||||
}, [setIsSettingsOpen]);
|
||||
|
||||
const handleDiscardChanges = useCallback((): void => {
|
||||
setAddedFilters(customFilters);
|
||||
}, [customFilters, setAddedFilters]);
|
||||
setAddedFilters(normalizedCustomFilters);
|
||||
}, [normalizedCustomFilters, setAddedFilters]);
|
||||
|
||||
const hasUnsavedChanges = useMemo(
|
||||
() =>
|
||||
!(
|
||||
addedFilters.length === normalizedCustomFilters.length &&
|
||||
addedFilters.every(
|
||||
(filter, index) => filter.key === normalizedCustomFilters[index].key,
|
||||
)
|
||||
),
|
||||
[addedFilters, normalizedCustomFilters],
|
||||
);
|
||||
|
||||
const handleSaveChanges = useCallback((): void => {
|
||||
if (signal) {
|
||||
updateCustomFilters({
|
||||
pathParams: { source: signal },
|
||||
data: {
|
||||
// Send only the stored TelemetryFieldKey fields; the composite `key`
|
||||
// is UI-only.
|
||||
filters: addedFilters.map((filter) => ({
|
||||
key: filter.key,
|
||||
datatype: filter.dataType,
|
||||
type: filter.type,
|
||||
name: filter.name,
|
||||
fieldContext: filter.fieldContext as TelemetrytypesFieldContextDTO,
|
||||
fieldDataType: filter.fieldDataType as TelemetrytypesFieldDataTypeDTO,
|
||||
})),
|
||||
signal,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -102,6 +135,7 @@ const useQuickFilterSettings = ({
|
||||
addedFilters,
|
||||
setAddedFilters,
|
||||
handleSaveChanges,
|
||||
hasUnsavedChanges,
|
||||
isUpdatingCustomFilters,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import getCustomFilters from 'api/quickFilters/getCustomFilters';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||
import { useGetQuickFilters } from 'api/generated/services/quick-filter';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
import { IQuickFiltersConfig, SignalType } from '../types';
|
||||
import { getFilterConfig } from '../utils';
|
||||
@@ -13,7 +11,7 @@ interface UseFilterConfigProps {
|
||||
}
|
||||
interface UseFilterConfigReturn {
|
||||
filterConfig: IQuickFiltersConfig[];
|
||||
customFilters: FilterType[];
|
||||
customFilters: TelemetryFieldKey[];
|
||||
isCustomFiltersLoading: boolean;
|
||||
isDynamicFilters: boolean;
|
||||
refetchCustomFilters: () => void;
|
||||
@@ -25,17 +23,16 @@ const useFilterConfig = ({
|
||||
}: UseFilterConfigProps): UseFilterConfigReturn => {
|
||||
const {
|
||||
isFetching: isCustomFiltersLoading,
|
||||
data: customFilters = [],
|
||||
data,
|
||||
refetch,
|
||||
} = useQuery<FilterType[], Error>(
|
||||
[REACT_QUERY_KEY.GET_CUSTOM_FILTERS, signal],
|
||||
async () => {
|
||||
const res = await getCustomFilters({ signal: signal || '' });
|
||||
return 'payload' in res && res.payload?.filters ? res.payload.filters : [];
|
||||
},
|
||||
{
|
||||
enabled: !!signal,
|
||||
},
|
||||
} = useGetQuickFilters(
|
||||
{ source: signal ?? '' },
|
||||
{ query: { enabled: !!signal } },
|
||||
);
|
||||
|
||||
const customFilters = useMemo<TelemetryFieldKey[]>(
|
||||
() => (data?.data?.filters ?? []) as TelemetryFieldKey[],
|
||||
[data],
|
||||
);
|
||||
|
||||
const isDynamicFilters = useMemo(
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from 'mocks-server/__mockdata__/customQuickFilters';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
@@ -34,9 +34,9 @@ const mockUseApiMonitoringParams = jest.mocked(useApiMonitoringParams);
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
const SIGNAL = SignalType.LOGS;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/${SIGNAL}`;
|
||||
const saveQuickFiltersURL = `${BASE_URL}/api/v1/orgs/me/filters`;
|
||||
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v3/filter_suggestions`;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
|
||||
const saveQuickFiltersURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
|
||||
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v1/fields/keys`;
|
||||
const quickFiltersAttributeValuesURL = `${BASE_URL}/api/v3/autocomplete/attribute_values`;
|
||||
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
|
||||
|
||||
@@ -338,6 +338,63 @@ describe('Quick Filters with custom filters', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps same-name fields with different context as distinct entries', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
server.use(
|
||||
rest.get(quickFiltersSuggestionsURL, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
keys: {
|
||||
level: [
|
||||
{
|
||||
name: 'level',
|
||||
fieldContext: 'attribute',
|
||||
fieldDataType: 'string',
|
||||
signal: 'logs',
|
||||
},
|
||||
{
|
||||
name: 'level',
|
||||
fieldContext: 'span',
|
||||
fieldDataType: 'string',
|
||||
signal: 'logs',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
const settingsButton = icon.closest('button') ?? icon;
|
||||
await user.click(settingsButton);
|
||||
|
||||
const otherSection = screen.getByText(OTHER_FILTERS_LABEL).parentElement!;
|
||||
// Both `level` variants are shown despite sharing a name.
|
||||
await waitFor(() =>
|
||||
expect(within(otherSection).getAllByText('level')).toHaveLength(2),
|
||||
);
|
||||
|
||||
// Adding one variant removes only that one; the other stays.
|
||||
const firstLevel = within(otherSection).getAllByText('level')[0];
|
||||
const addButton = firstLevel.parentElement?.querySelector('button');
|
||||
await user.click(addButton as HTMLButtonElement);
|
||||
|
||||
const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!;
|
||||
await waitFor(() => {
|
||||
expect(within(addedSection).getAllByText('level')).toHaveLength(1);
|
||||
expect(within(otherSection).getAllByText('level')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('adds a filter from OTHER FILTERS to ADDED FILTERS when clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
@@ -456,12 +513,10 @@ describe('Quick Filters with custom filters', () => {
|
||||
});
|
||||
|
||||
const requestBody = putHandler.mock.calls[0][0];
|
||||
expect(requestBody.filters).toStrictEqual(
|
||||
expect.arrayContaining([
|
||||
expect.not.objectContaining({ key: FILTER_OS_DESCRIPTION }),
|
||||
]),
|
||||
expect(requestBody.filters).not.toContainEqual(
|
||||
expect.objectContaining({ name: FILTER_OS_DESCRIPTION }),
|
||||
);
|
||||
expect(requestBody.signal).toBe(SIGNAL);
|
||||
expect(requestBody.filters).toHaveLength(10);
|
||||
});
|
||||
|
||||
it('should render duration slider for duration_nono filter', async () => {
|
||||
@@ -612,9 +667,9 @@ describe('Quick Filters refetch behavior', () => {
|
||||
filters: [
|
||||
...(quickFiltersListResponse.data.filters ?? []),
|
||||
{
|
||||
key: 'new.custom.filter',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
name: 'new.custom.filter',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: 'resource',
|
||||
} as const,
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,17 +1,33 @@
|
||||
import { TelemetrytypesFieldContextDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
|
||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { fieldDataTypeToDataType } from 'utils/fieldDataType';
|
||||
|
||||
import { FiltersType, IQuickFiltersConfig, SignalType } from './types';
|
||||
|
||||
const FILTER_TITLE_MAP: Record<string, string> = {
|
||||
duration_nano: 'Duration',
|
||||
hasError: 'Has Error (Status)',
|
||||
has_error: 'Has Error (Status)',
|
||||
};
|
||||
|
||||
const FILTER_TYPE_MAP: Record<string, FiltersType> = {
|
||||
duration_nano: FiltersType.DURATION,
|
||||
};
|
||||
|
||||
// The map below exists only for the old v3 attribute-values fetch
|
||||
// (useCheckboxFilterValues), the sole reader of attributeKey.dataType/type.
|
||||
// Once the values fetch moves to fields/values, remove this and reduce
|
||||
// attributeKey to { id, key }.
|
||||
|
||||
const FIELD_CONTEXT_TO_ATTRIBUTE_TYPE: Record<string, string> = {
|
||||
[TelemetrytypesFieldContextDTO.attribute]: 'tag',
|
||||
[TelemetrytypesFieldContextDTO.resource]: 'resource',
|
||||
};
|
||||
|
||||
const mapFieldContext = (fieldContext?: string): string =>
|
||||
(fieldContext && FIELD_CONTEXT_TO_ATTRIBUTE_TYPE[fieldContext]) || '';
|
||||
|
||||
const getFilterName = (str: string): string => {
|
||||
if (FILTER_TITLE_MAP[str]) {
|
||||
return FILTER_TITLE_MAP[str];
|
||||
@@ -26,16 +42,16 @@ const getFilterName = (str: string): string => {
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
const getFilterType = (att: FilterType): FiltersType => {
|
||||
if (FILTER_TYPE_MAP[att.key]) {
|
||||
return FILTER_TYPE_MAP[att.key];
|
||||
const getFilterType = (att: TelemetryFieldKey): FiltersType => {
|
||||
if (FILTER_TYPE_MAP[att.name]) {
|
||||
return FILTER_TYPE_MAP[att.name];
|
||||
}
|
||||
return FiltersType.CHECKBOX;
|
||||
};
|
||||
|
||||
export const getFilterConfig = (
|
||||
signal?: SignalType,
|
||||
customFilters?: FilterType[],
|
||||
customFilters?: TelemetryFieldKey[],
|
||||
config?: IQuickFiltersConfig[],
|
||||
): IQuickFiltersConfig[] => {
|
||||
if (!customFilters?.length || !signal) {
|
||||
@@ -46,13 +62,13 @@ export const getFilterConfig = (
|
||||
(att, index) =>
|
||||
({
|
||||
type: getFilterType(att),
|
||||
title: getFilterName(att.key),
|
||||
title: getFilterName(att.name),
|
||||
dataSource: SIGNAL_DATA_SOURCE_MAP[signal],
|
||||
attributeKey: {
|
||||
id: att.key,
|
||||
key: att.key,
|
||||
dataType: att.dataType,
|
||||
type: att.type,
|
||||
id: att.name,
|
||||
key: att.name,
|
||||
dataType: fieldDataTypeToDataType(att.fieldDataType),
|
||||
type: mapFieldContext(att.fieldContext),
|
||||
},
|
||||
defaultOpen: index < 2,
|
||||
}) as IQuickFiltersConfig,
|
||||
|
||||
@@ -4,14 +4,18 @@ import { refreshLicense } from 'api/generated/services/licenses';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { RefreshCcw } from '@signozhq/icons';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import { buildLicenseUpdatePermission } from 'lib/authz/hooks/useAuthZ/permissions/license.permissions';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
function RefreshPaymentStatus({
|
||||
type,
|
||||
className,
|
||||
withPortal,
|
||||
}: {
|
||||
type?: 'button' | 'text' | 'tooltip';
|
||||
className?: string;
|
||||
withPortal?: false;
|
||||
}): JSX.Element {
|
||||
const { t } = useTranslation(['failedPayment']);
|
||||
const { activeLicense, activeLicenseRefetch } = useAppContext();
|
||||
@@ -36,17 +40,25 @@ function RefreshPaymentStatus({
|
||||
};
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
variant="link"
|
||||
color={type === 'text' ? 'none' : 'secondary'}
|
||||
size="md"
|
||||
className={className}
|
||||
onClick={handleRefreshPaymentStatus}
|
||||
prefix={<RefreshCcw size={14} />}
|
||||
loading={isLoading}
|
||||
<AuthZTooltip
|
||||
checks={
|
||||
activeLicense ? [buildLicenseUpdatePermission(activeLicense.id)] : []
|
||||
}
|
||||
enabled={!!activeLicense}
|
||||
withPortal={withPortal}
|
||||
>
|
||||
{type !== 'tooltip' ? t('refreshPaymentStatus') : ''}
|
||||
</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>
|
||||
</AuthZTooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -62,6 +74,7 @@ function RefreshPaymentStatus({
|
||||
RefreshPaymentStatus.defaultProps = {
|
||||
type: 'button',
|
||||
className: undefined,
|
||||
withPortal: undefined,
|
||||
};
|
||||
|
||||
export default RefreshPaymentStatus;
|
||||
|
||||
@@ -348,6 +348,19 @@ export const initialQueryMeterWithType: Query = {
|
||||
},
|
||||
};
|
||||
|
||||
export const initialQueryAIWithType: Query = {
|
||||
...initialQueryWithType,
|
||||
builder: {
|
||||
...initialQueryWithType.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.traces,
|
||||
builderQueryType: 'builder_ai_query',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const operatorsByTypes: Record<LocalDataType, string[]> = {
|
||||
string: Object.values(StringOperators),
|
||||
number: Object.values(NumberOperators),
|
||||
|
||||
@@ -15,7 +15,6 @@ export const REACT_QUERY_KEY = {
|
||||
GET_ALL_DASHBOARDS: 'GET_ALL_DASHBOARDS',
|
||||
GET_TRIGGERED_ALERTS: 'GET_TRIGGERED_ALERTS',
|
||||
DASHBOARD_BY_ID: 'DASHBOARD_BY_ID',
|
||||
GET_BILLING_USAGE: 'GET_BILLING_USAGE',
|
||||
GET_FEATURES_FLAGS: 'GET_FEATURES_FLAGS',
|
||||
DELETE_DASHBOARD: 'DELETE_DASHBOARD',
|
||||
LOGS_PIPELINE_PREVIEW: 'LOGS_PIPELINE_PREVIEW',
|
||||
|
||||
@@ -184,6 +184,11 @@
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.6;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-restricted-banner,
|
||||
|
||||
@@ -20,7 +20,8 @@ import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import getChangelogByVersion from 'api/changelog/getChangelogByVersion';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import manageCreditCardApi from 'api/v1/portal/create';
|
||||
import { updateSubscription } from 'api/generated/services/subscriptions';
|
||||
import type { UpdateSubscription200 } from 'api/generated/services/sigNoz.schemas';
|
||||
import updateUserPreference from 'api/v1/user/preferences/name/update';
|
||||
import getUserVersion from 'api/v1/version/get';
|
||||
import getUserLatestVersion from 'api/v1/version/getLatestVersion';
|
||||
@@ -30,6 +31,8 @@ import ChangelogModal from 'components/ChangelogModal/ChangelogModal';
|
||||
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import RefreshPaymentStatus from 'components/RefreshPaymentStatus/RefreshPaymentStatus';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import { SubscriptionManagePermissions } from 'lib/authz/hooks/useAuthZ/permissions/subscription.permissions';
|
||||
import { MIN_ACCOUNT_AGE_FOR_CHANGELOG } from 'constants/changelog';
|
||||
import { Events } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
@@ -63,8 +66,7 @@ import {
|
||||
UPDATE_LATEST_VERSION,
|
||||
UPDATE_LATEST_VERSION_ERROR,
|
||||
} from 'types/actions/app';
|
||||
import { ErrorResponse, SuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
ChangelogSchema,
|
||||
DeploymentType,
|
||||
@@ -77,7 +79,6 @@ import {
|
||||
} from 'types/api/licensesV3/getActive';
|
||||
import { UserPreference } from 'types/api/preferences/preference';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
import { showErrorNotification } from 'utils/error';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
@@ -166,9 +167,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
return Math.abs(currentDate.diff(userCreationDate, 'day'));
|
||||
}, [user.createdAt]);
|
||||
|
||||
const handleBillingOnSuccess = (
|
||||
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
|
||||
): void => {
|
||||
const handleBillingOnSuccess = (data: UpdateSubscription200): void => {
|
||||
if (data?.data?.redirectURL) {
|
||||
const newTab = document.createElement('a');
|
||||
newTab.href = data.data.redirectURL;
|
||||
@@ -186,7 +185,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
};
|
||||
|
||||
const { mutate: manageCreditCard, isLoading: isLoadingManageBilling } =
|
||||
useMutation(manageCreditCardApi, {
|
||||
useMutation(updateSubscription, {
|
||||
onSuccess: (data) => {
|
||||
handleBillingOnSuccess(data);
|
||||
},
|
||||
@@ -469,10 +468,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
}, [isLoggedIn]);
|
||||
|
||||
const handleUpgrade = useCallback((): void => {
|
||||
if (user.role === USER_ROLES.ADMIN) {
|
||||
history.push(ROUTES.BILLING);
|
||||
}
|
||||
}, [user.role]);
|
||||
history.push(ROUTES.BILLING);
|
||||
}, []);
|
||||
|
||||
const handleFailedPayment = useCallback((): void => {
|
||||
manageCreditCard({
|
||||
@@ -586,25 +583,21 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
<div>
|
||||
Our systems are taking longer than expected for your trial workspace.
|
||||
Please{' '}
|
||||
{user.role === USER_ROLES.ADMIN ? (
|
||||
<span>
|
||||
<a
|
||||
className="upgrade-link"
|
||||
onClick={(): void => {
|
||||
notifications.destroy('slow-api-warning');
|
||||
<span>
|
||||
<a
|
||||
className="upgrade-link"
|
||||
onClick={(): void => {
|
||||
notifications.destroy('slow-api-warning');
|
||||
|
||||
logEvent(`Slow API Banner: Upgrade clicked`, {});
|
||||
logEvent(`Slow API Banner: Upgrade clicked`, {});
|
||||
|
||||
handleUpgrade();
|
||||
}}
|
||||
>
|
||||
upgrade
|
||||
</a>
|
||||
your workspace for a smoother experience.
|
||||
</span>
|
||||
) : (
|
||||
'contact your administrator for upgrading to a paid plan for a smoother experience.'
|
||||
)}
|
||||
handleUpgrade();
|
||||
}}
|
||||
>
|
||||
upgrade
|
||||
</a>
|
||||
your workspace for a smoother experience.
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
duration: 60000,
|
||||
@@ -794,22 +787,18 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
<div className="trial-expiry-banner">
|
||||
You are in free trial period. Your free trial will end on{' '}
|
||||
<span>{getFormattedDate(trialInfo?.trialEnd || Date.now())}.</span>
|
||||
{user.role === USER_ROLES.ADMIN ? (
|
||||
<span>
|
||||
<span>
|
||||
{' '}
|
||||
Please{' '}
|
||||
<a className="upgrade-link" onClick={handleUpgrade}>
|
||||
upgrade
|
||||
</a>
|
||||
to continue using SigNoz features.
|
||||
<span className="refresh-payment-status">
|
||||
{' '}
|
||||
Please{' '}
|
||||
<a className="upgrade-link" onClick={handleUpgrade}>
|
||||
upgrade
|
||||
</a>
|
||||
to continue using SigNoz features.
|
||||
<span className="refresh-payment-status">
|
||||
{' '}
|
||||
| Already upgraded? <RefreshPaymentStatus type="text" />
|
||||
</span>
|
||||
| Already upgraded? <RefreshPaymentStatus type="text" />
|
||||
</span>
|
||||
) : (
|
||||
'Please contact your administrator for upgrading to a paid plan.'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -826,22 +815,20 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
)}
|
||||
.
|
||||
</span>
|
||||
{user.role === USER_ROLES.ADMIN ? (
|
||||
<span>
|
||||
{' '}
|
||||
Please{' '}
|
||||
<span>
|
||||
{' '}
|
||||
Please{' '}
|
||||
<AuthZTooltip checks={SubscriptionManagePermissions}>
|
||||
<a className="upgrade-link" onClick={handleFailedPayment}>
|
||||
pay the bill
|
||||
</a>
|
||||
to continue using SigNoz features.
|
||||
<span className="refresh-payment-status">
|
||||
{' '}
|
||||
| Already paid? <RefreshPaymentStatus type="text" />
|
||||
</span>
|
||||
</AuthZTooltip>
|
||||
to continue using SigNoz features.
|
||||
<span className="refresh-payment-status">
|
||||
{' '}
|
||||
| Already paid? <RefreshPaymentStatus type="text" />
|
||||
</span>
|
||||
) : (
|
||||
' Please contact your administrator to pay the bill.'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
SubscriptionCreatePermission,
|
||||
SubscriptionReadPermission,
|
||||
SubscriptionUpdatePermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/subscription.permissions';
|
||||
import {
|
||||
setupAuthzAdmin,
|
||||
setupAuthzAllow,
|
||||
setupAuthzDeny,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
import { trialConvertedToSubscriptionResponse } from 'mocks-server/__mockdata__/licenses';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
import BillingContainer from './BillingContainer';
|
||||
|
||||
window.ResizeObserver =
|
||||
window.ResizeObserver ||
|
||||
jest.fn().mockImplementation(() => ({
|
||||
disconnect: jest.fn(),
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('BillingContainer - AuthZ', () => {
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('renders usage and enables actions when all subscription permissions are granted', async () => {
|
||||
server.use(setupAuthzAdmin());
|
||||
|
||||
render(<BillingContainer />);
|
||||
|
||||
await expect(
|
||||
screen.findByRole('columnheader', { name: /data ingested/i }),
|
||||
).resolves.toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('header-billing-button')).toBeEnabled();
|
||||
});
|
||||
expect(screen.queryByText(/not authorized/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('blocks the usage section when subscription read is denied', async () => {
|
||||
server.use(setupAuthzDeny(SubscriptionReadPermission));
|
||||
|
||||
render(<BillingContainer />);
|
||||
|
||||
await expect(
|
||||
screen.findByText(/not authorized/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(screen.getByTestId('header-billing-button')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('columnheader', { name: /data ingested/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables upgrade when subscription create is denied', async () => {
|
||||
server.use(setupAuthzAllow(SubscriptionReadPermission));
|
||||
|
||||
render(<BillingContainer />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('header-billing-button')).toBeDisabled();
|
||||
});
|
||||
expect(screen.getByTestId('upgrade-plan-button')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables manage billing when subscription update is denied', async () => {
|
||||
server.use(
|
||||
setupAuthzAllow(SubscriptionReadPermission, SubscriptionCreatePermission),
|
||||
);
|
||||
|
||||
render(
|
||||
<BillingContainer />,
|
||||
{},
|
||||
{
|
||||
appContextOverrides: {
|
||||
trialInfo: trialConvertedToSubscriptionResponse.data,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('header-billing-button')).toBeDisabled();
|
||||
});
|
||||
expect(screen.queryByTestId('upgrade-plan-button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables manage billing when subscription list is denied', async () => {
|
||||
server.use(
|
||||
setupAuthzAllow(
|
||||
SubscriptionReadPermission,
|
||||
SubscriptionCreatePermission,
|
||||
SubscriptionUpdatePermission,
|
||||
),
|
||||
);
|
||||
|
||||
render(
|
||||
<BillingContainer />,
|
||||
{},
|
||||
{
|
||||
appContextOverrides: {
|
||||
trialInfo: trialConvertedToSubscriptionResponse.data,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('header-billing-button')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@
|
||||
margin: 0 auto var(--spacing-20);
|
||||
|
||||
.pageHeader {
|
||||
margin-bottom: var(--spacing-8);
|
||||
margin-bottom: var(--spacing-4);
|
||||
|
||||
.pageHeaderTitle {
|
||||
font-weight: var(--label-medium-500-font-weight);
|
||||
@@ -41,6 +41,8 @@
|
||||
}
|
||||
|
||||
.pageInfo {
|
||||
margin-bottom: var(--spacing-4);
|
||||
|
||||
:global(.ant-card) {
|
||||
padding: var(--padding-3);
|
||||
}
|
||||
@@ -58,8 +60,12 @@
|
||||
margin: var(--spacing-12) var(--spacing-4);
|
||||
}
|
||||
|
||||
.usageDenied {
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.billingDetails {
|
||||
margin: var(--spacing-12) 0;
|
||||
margin: var(--spacing-4) 0;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
@@ -128,7 +134,7 @@
|
||||
}
|
||||
|
||||
.upgradePlanBenefits {
|
||||
margin: 0 var(--spacing-4);
|
||||
margin: 0;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 5px;
|
||||
padding: 0 var(--padding-12);
|
||||
@@ -176,7 +182,7 @@
|
||||
}
|
||||
|
||||
.billingUpdateNote {
|
||||
margin-top: var(--spacing-8);
|
||||
margin-top: var(--spacing-4);
|
||||
font-family: var(--font-family-inter);
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
|
||||
import { billingSuccessResponse } from 'mocks-server/__mockdata__/billing';
|
||||
import {
|
||||
licensesSuccessResponse,
|
||||
notOfTrailResponse,
|
||||
trialConvertedToSubscriptionResponse,
|
||||
} from 'mocks-server/__mockdata__/licenses';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { act, render, screen, getAppContextMock } from 'tests/test-utils';
|
||||
import APIError from 'types/api/error';
|
||||
import {
|
||||
@@ -15,11 +17,6 @@ import { getFormattedDate } from 'utils/timeUtils';
|
||||
|
||||
import BillingContainer from './BillingContainer';
|
||||
|
||||
jest.mock('hooks/useActiveLicenseKey/useActiveLicenseKey', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => ({ licenseKey: 'test-key', isLoading: false })),
|
||||
}));
|
||||
|
||||
window.ResizeObserver =
|
||||
window.ResizeObserver ||
|
||||
jest.fn().mockImplementation(() => ({
|
||||
@@ -31,14 +28,22 @@ window.ResizeObserver =
|
||||
describe('BillingContainer', () => {
|
||||
jest.setTimeout(30000);
|
||||
|
||||
beforeEach(() => {
|
||||
server.use(setupAuthzAdmin());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('Component should render', async () => {
|
||||
render(<BillingContainer />);
|
||||
|
||||
const dataInjection = screen.getByRole('columnheader', {
|
||||
const dataInjection = await screen.findByRole('columnheader', {
|
||||
name: /data ingested/i,
|
||||
});
|
||||
expect(dataInjection).toBeInTheDocument();
|
||||
const pricePerUnit = screen.getByRole('columnheader', {
|
||||
const pricePerUnit = await screen.findByRole('columnheader', {
|
||||
name: /price per unit/i,
|
||||
});
|
||||
expect(pricePerUnit).toBeInTheDocument();
|
||||
@@ -49,13 +54,15 @@ describe('BillingContainer', () => {
|
||||
|
||||
const dayRemainingInBillingPeriod = await screen.findByText(
|
||||
/Please upgrade plan now to retain your data./i,
|
||||
{},
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
|
||||
|
||||
const upgradePlanButton = screen.getByTestId('upgrade-plan-button');
|
||||
expect(upgradePlanButton).toBeInTheDocument();
|
||||
|
||||
const dollar = await screen.findByText(/\$1,278.3/i);
|
||||
const dollar = await screen.findByText(/\$1,278.3/i, {}, { timeout: 5000 });
|
||||
expect(dollar).toBeInTheDocument();
|
||||
|
||||
const currentBill = await screen.findByText('billing');
|
||||
@@ -86,7 +93,9 @@ describe('BillingContainer', () => {
|
||||
|
||||
await expect(screen.findByText('Free Trial')).resolves.toBeInTheDocument();
|
||||
await expect(screen.findByText('billing')).resolves.toBeInTheDocument();
|
||||
await expect(screen.findByText(/\$0/i)).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText(/\$0/i, {}, { timeout: 5000 }),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
await expect(
|
||||
screen.findByText(
|
||||
@@ -132,7 +141,7 @@ describe('BillingContainer', () => {
|
||||
const currentBill = await screen.findByText('billing');
|
||||
expect(currentBill).toBeInTheDocument();
|
||||
|
||||
const dollar0 = await screen.findByText(/\$0/i);
|
||||
const dollar0 = await screen.findByText(/\$0/i, {}, { timeout: 5000 });
|
||||
expect(dollar0).toBeInTheDocument();
|
||||
|
||||
const onTrail = await screen.findByText(
|
||||
@@ -250,7 +259,11 @@ describe('BillingContainer', () => {
|
||||
billingSuccessResponse.data.billingPeriodStart,
|
||||
)} to ${getFormattedDate(billingSuccessResponse.data.billingPeriodEnd)}`;
|
||||
|
||||
const billingPeriod = await findByText(billingPeriodText);
|
||||
const billingPeriod = await findByText(
|
||||
billingPeriodText,
|
||||
{},
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
expect(billingPeriod).toBeInTheDocument();
|
||||
|
||||
const currentBill = await screen.findByText('billing');
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { useMutation } from 'react-query';
|
||||
import { CircleCheck, Landmark, MonitorDown } from '@signozhq/icons';
|
||||
import {
|
||||
Card,
|
||||
@@ -15,25 +15,36 @@ import {
|
||||
TableColumnsType as ColumnsType,
|
||||
} from 'antd';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import getUsage, {
|
||||
BreakdownEntry,
|
||||
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';
|
||||
import type {
|
||||
CreateSubscription201,
|
||||
GetSubscription200,
|
||||
SubscriptiontypesGettableSubscriptionUsageDTO,
|
||||
SubscriptiontypesSubscriptionUsageBreakdownDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
createSubscription,
|
||||
updateSubscription,
|
||||
useGetSubscription,
|
||||
} from 'api/generated/services/subscriptions';
|
||||
import RefreshPaymentStatus from 'components/RefreshPaymentStatus/RefreshPaymentStatus';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { isEmpty, pick } from 'lodash-es';
|
||||
import useActiveLicenseKey from 'hooks/useActiveLicenseKey/useActiveLicenseKey';
|
||||
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import { AuthZGuardContent } from 'lib/authz/components/AuthZGuard/AuthZGuardContent';
|
||||
import PermissionDeniedCallout from 'lib/authz/components/PermissionDeniedCallout/PermissionDeniedCallout';
|
||||
import {
|
||||
SubscriptionCreatePermission,
|
||||
SubscriptionManagePermissions,
|
||||
SubscriptionReadPermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/subscription.permissions';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { ErrorResponse, SuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
||||
|
||||
@@ -135,7 +146,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
const [isFreeTrial, setIsFreeTrial] = useState(false);
|
||||
const [data, setData] = useState<DataType[]>([]);
|
||||
const [apiResponse, setApiResponse] = useState<
|
||||
Partial<UsageResponsePayloadProps>
|
||||
Partial<SubscriptiontypesGettableSubscriptionUsageDTO>
|
||||
>({});
|
||||
|
||||
const {
|
||||
@@ -146,7 +157,8 @@ export default function BillingContainer(): JSX.Element {
|
||||
activeLicense,
|
||||
activeLicenseFetchError,
|
||||
} = useAppContext();
|
||||
const { licenseKey } = useActiveLicenseKey();
|
||||
const { allowed: canReadSubscription, error: subscriptionAuthZError } =
|
||||
useAuthZ([SubscriptionReadPermission]);
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const handleError = useAxiosError();
|
||||
@@ -154,33 +166,34 @@ export default function BillingContainer(): JSX.Element {
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const processUsageData = useCallback(
|
||||
(data: SuccessResponse<UsageResponsePayloadProps> | ErrorResponse): void => {
|
||||
if (isEmpty(data?.payload)) {
|
||||
(response: GetSubscription200): void => {
|
||||
const usage = response?.data;
|
||||
if (isEmpty(usage)) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
details: { breakdown = [], billTotal },
|
||||
billingPeriodStart,
|
||||
billingPeriodEnd,
|
||||
} = (data as SuccessResponse<UsageResponsePayloadProps>).payload;
|
||||
const breakdown = usage.details?.breakdown ?? [];
|
||||
const billTotal = usage.details?.billTotal ?? 0;
|
||||
const billingPeriodStart = usage.billingPeriodStart ?? 0;
|
||||
const billingPeriodEnd = usage.billingPeriodEnd ?? 0;
|
||||
const formattedUsageData: DataType[] = [];
|
||||
|
||||
if (breakdown && Array.isArray(breakdown)) {
|
||||
for (let index = 0; index < breakdown.length; index += 1) {
|
||||
const element: BreakdownEntry = breakdown[index];
|
||||
|
||||
element?.tiers?.forEach((tier, i: number) => {
|
||||
breakdown.forEach(
|
||||
(
|
||||
element: SubscriptiontypesSubscriptionUsageBreakdownDTO,
|
||||
index: number,
|
||||
) => {
|
||||
element?.tiers?.forEach((tier, tierIndex: number) => {
|
||||
formattedUsageData.push({
|
||||
key: `${index}${i}`,
|
||||
name: i === 0 ? element?.type : '',
|
||||
key: `${index}${tierIndex}`,
|
||||
name: tierIndex === 0 ? (element?.type ?? '') : '',
|
||||
unit: element?.unit ?? '',
|
||||
dataIngested: `${tier.quantity} ${element?.unit}`,
|
||||
pricePerUnit: String(tier.unitPrice),
|
||||
cost: `$ ${tier.tierCost}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
setData(formattedUsageData);
|
||||
|
||||
@@ -196,7 +209,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
setBillAmount(billTotal);
|
||||
}
|
||||
|
||||
setApiResponse(data?.payload || {});
|
||||
setApiResponse(usage);
|
||||
},
|
||||
[trialInfo?.onTrial],
|
||||
);
|
||||
@@ -208,11 +221,12 @@ export default function BillingContainer(): JSX.Element {
|
||||
isLoading,
|
||||
isFetching: isFetchingBillingData,
|
||||
data: billingData,
|
||||
} = useQuery([REACT_QUERY_KEY.GET_BILLING_USAGE, user?.id], {
|
||||
queryFn: () => getUsage(licenseKey || ''),
|
||||
onError: handleError,
|
||||
enabled: !!licenseKey,
|
||||
onSuccess: processUsageData,
|
||||
} = useGetSubscription({
|
||||
query: {
|
||||
enabled: canReadSubscription || !!subscriptionAuthZError,
|
||||
onError: handleError,
|
||||
onSuccess: processUsageData,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -284,9 +298,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
/>
|
||||
);
|
||||
|
||||
const handleBillingOnSuccess = (
|
||||
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
|
||||
): void => {
|
||||
const handleBillingOnSuccess = (data: CreateSubscription201): void => {
|
||||
if (data?.data?.redirectURL) {
|
||||
const newTab = document.createElement('a');
|
||||
newTab.href = data.data.redirectURL;
|
||||
@@ -303,7 +315,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
};
|
||||
|
||||
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
|
||||
updateCreditCardApi,
|
||||
createSubscription,
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
handleBillingOnSuccess(data);
|
||||
@@ -313,7 +325,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
);
|
||||
|
||||
const { mutate: manageCreditCard, isLoading: isLoadingManageBilling } =
|
||||
useMutation(manageCreditCardApi, {
|
||||
useMutation(updateSubscription, {
|
||||
onSuccess: (data) => {
|
||||
handleBillingOnSuccess(data);
|
||||
},
|
||||
@@ -348,15 +360,21 @@ export default function BillingContainer(): JSX.Element {
|
||||
updateCreditCard,
|
||||
]);
|
||||
|
||||
const billingActionPermissions = trialInfo?.trialConvertedToSubscription
|
||||
? SubscriptionManagePermissions
|
||||
: [SubscriptionCreatePermission];
|
||||
|
||||
const subscriptionPastDueMessage = (): JSX.Element => (
|
||||
<Typography>
|
||||
{`We were not able to process payments for your account. Please update your card details `}
|
||||
<Typography.Link
|
||||
onClick={handleBilling}
|
||||
style={{ cursor: 'pointer', color: 'var(--bg-cherry-500)' }}
|
||||
>
|
||||
{t('here')}
|
||||
</Typography.Link>
|
||||
<AuthZTooltip checks={billingActionPermissions}>
|
||||
<Typography.Link
|
||||
onClick={handleBilling}
|
||||
style={{ cursor: 'pointer', color: 'var(--bg-cherry-500)' }}
|
||||
>
|
||||
{t('here')}
|
||||
</Typography.Link>
|
||||
</AuthZTooltip>
|
||||
{` if your payment information has changed. Email us at `}
|
||||
<Typography.Text color="muted">cloud-support@signoz.io</Typography.Text>
|
||||
{` otherwise. Be sure to provide this information immediately to avoid interruption to your service.`}
|
||||
@@ -411,11 +429,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{ minHeight: 150, marginBottom: 16 }}
|
||||
className={styles.pageInfo}
|
||||
>
|
||||
<Card bordered={false} className={styles.pageInfo}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Flex vertical gap={8}>
|
||||
<p className={styles.pageInfoTitle}>
|
||||
@@ -423,13 +437,14 @@ export default function BillingContainer(): JSX.Element {
|
||||
{isFreeTrial ? <Badge color="success"> Free Trial </Badge> : ''}
|
||||
</p>
|
||||
|
||||
{!isLoading && !isFetchingBillingData && !showGracePeriodMessage ? (
|
||||
{billingData && !isFetchingBillingData && !showGracePeriodMessage ? (
|
||||
<p className={styles.pageInfoSubtitle}>
|
||||
{daysRemaining} {daysRemainingStr}
|
||||
</p>
|
||||
) : null}
|
||||
</Flex>
|
||||
<Button
|
||||
<AuthZButton
|
||||
checks={billingActionPermissions}
|
||||
testId="header-billing-button"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
@@ -443,7 +458,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
{trialInfo?.trialConvertedToSubscription
|
||||
? t('manage_billing')
|
||||
: t('upgrade_plan')}
|
||||
</Button>
|
||||
</AuthZButton>
|
||||
</Flex>
|
||||
|
||||
{trialInfo?.onTrial && trialInfo?.trialConvertedToSubscription && (
|
||||
@@ -495,66 +510,81 @@ export default function BillingContainer(): JSX.Element {
|
||||
))}
|
||||
</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>
|
||||
)}
|
||||
|
||||
<div className={styles.billingDetails}>
|
||||
{!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}`}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
}}
|
||||
<AuthZGuardContent
|
||||
checks={[SubscriptionReadPermission]}
|
||||
fallback={({ deniedPermissions }): JSX.Element => (
|
||||
<PermissionDeniedCallout
|
||||
deniedPermissions={deniedPermissions}
|
||||
className={styles.usageDenied}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{(isLoading || isFetchingBillingData) && renderTableSkeleton()}
|
||||
</div>
|
||||
<div className={styles.billingDetails}>
|
||||
{!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}`}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(isLoading || isFetchingBillingData) && renderTableSkeleton()}
|
||||
</div>
|
||||
</>
|
||||
</AuthZGuardContent>
|
||||
|
||||
{isCloudUserVal && activeLicense?.state === LicenseState.ACTIVATED && (
|
||||
<CancelSubscriptionBanner />
|
||||
@@ -597,7 +627,8 @@ export default function BillingContainer(): JSX.Element {
|
||||
</Typography.Text>
|
||||
</Col>
|
||||
<Col span={4} style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
<AuthZButton
|
||||
checks={[SubscriptionCreatePermission]}
|
||||
testId="upgrade-plan-button"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
@@ -606,7 +637,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
onClick={handleBilling}
|
||||
>
|
||||
{t('upgrade_plan')}
|
||||
</Button>
|
||||
</AuthZButton>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from 'lib/uPlotV2/components/types';
|
||||
import type { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import type uPlot from 'uplot';
|
||||
import type { UsageResponsePayloadProps } from 'api/billing/getUsage';
|
||||
import type { SubscriptiontypesGettableSubscriptionUsageDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { BillingBarChartTooltip } from './BillingBarChartTooltip';
|
||||
import { prepareBillingBarConfig } from './prepareBillingBarConfig';
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import styles from './BillingUsageGraph.module.scss';
|
||||
|
||||
interface BillingUsageGraphProps {
|
||||
data: Partial<UsageResponsePayloadProps>;
|
||||
data: Partial<SubscriptiontypesGettableSubscriptionUsageDTO>;
|
||||
billAmount: number;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
|
||||
const currentDay = breakdown.dayWiseBreakdown.breakdown[0];
|
||||
const nextDay = {
|
||||
...currentDay,
|
||||
timestamp: currentDay.timestamp + 86400,
|
||||
timestamp: (currentDay.timestamp ?? 0) + 86400,
|
||||
count: 0,
|
||||
size: 0,
|
||||
quantity: 0,
|
||||
@@ -94,7 +94,9 @@ export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
|
||||
|
||||
const { startTime, endTime } = useMemo(
|
||||
() =>
|
||||
calculateStartEndTime(normalizedData as Partial<UsageResponsePayloadProps>),
|
||||
calculateStartEndTime(
|
||||
normalizedData as Partial<SubscriptiontypesGettableSubscriptionUsageDTO>,
|
||||
),
|
||||
[normalizedData],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UsageResponsePayloadProps } from 'api/billing/getUsage';
|
||||
import { SubscriptiontypesGettableSubscriptionUsageDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import dayjs from 'dayjs';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
@@ -117,7 +117,9 @@ export function csvFileName(csvData: QuantityData[]): string {
|
||||
return `billing_usage_(${startDate}-${endDate}).csv`;
|
||||
}
|
||||
|
||||
export function prepareCsvData(data: Partial<UsageResponsePayloadProps>): {
|
||||
export function prepareCsvData(
|
||||
data: Partial<SubscriptiontypesGettableSubscriptionUsageDTO>,
|
||||
): {
|
||||
csvData: string;
|
||||
fileName: string;
|
||||
} {
|
||||
@@ -135,12 +137,14 @@ export function prepareCsvData(data: Partial<UsageResponsePayloadProps>): {
|
||||
}
|
||||
|
||||
export function calculateStartEndTime(
|
||||
data: Partial<UsageResponsePayloadProps>,
|
||||
data: Partial<SubscriptiontypesGettableSubscriptionUsageDTO>,
|
||||
): { startTime: number | undefined; endTime: number | undefined } {
|
||||
const timestamps: number[] = [];
|
||||
data?.details?.breakdown?.forEach((breakdown) => {
|
||||
breakdown?.dayWiseBreakdown?.breakdown?.forEach((entry) => {
|
||||
timestamps.push(entry.timestamp);
|
||||
if (typeof entry.timestamp === 'number') {
|
||||
timestamps.push(entry.timestamp);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background-color: var(--l2-background);
|
||||
margin: var(--spacing-4) 0 var(--spacing-12);
|
||||
margin: var(--spacing-4) 0;
|
||||
}
|
||||
|
||||
.info {
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { SubscriptionDeletePermission } from 'lib/authz/hooks/useAuthZ/permissions/subscription.permissions';
|
||||
import {
|
||||
setupAuthzAdmin,
|
||||
setupAuthzDeny,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import CancelSubscriptionBanner from './CancelSubscriptionBanner';
|
||||
@@ -36,10 +42,24 @@ function mockMailto(): {
|
||||
}
|
||||
|
||||
describe('CancelSubscriptionBanner', () => {
|
||||
beforeEach(() => {
|
||||
server.use(setupAuthzAdmin());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('disables Cancel Subscription when subscription delete is denied', async () => {
|
||||
server.use(setupAuthzDeny(SubscriptionDeletePermission));
|
||||
render(<CancelSubscriptionBanner />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('cancel-subscription-btn')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders banner with title and subtitle', () => {
|
||||
render(<CancelSubscriptionBanner />);
|
||||
expect(
|
||||
@@ -56,9 +76,10 @@ describe('CancelSubscriptionBanner', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<CancelSubscriptionBanner />);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /cancel subscription/i }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('cancel-subscription-btn')).toBeEnabled();
|
||||
});
|
||||
await user.click(screen.getByTestId('cancel-subscription-btn'));
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(
|
||||
@@ -76,9 +97,10 @@ describe('CancelSubscriptionBanner', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<CancelSubscriptionBanner />);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /cancel subscription/i }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('cancel-subscription-btn')).toBeEnabled();
|
||||
});
|
||||
await user.click(screen.getByTestId('cancel-subscription-btn'));
|
||||
|
||||
const confirmButton = screen.getByTestId('cancel-subscription-confirm-btn');
|
||||
expect(confirmButton).toBeDisabled();
|
||||
@@ -95,9 +117,10 @@ describe('CancelSubscriptionBanner', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<CancelSubscriptionBanner />);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /cancel subscription/i }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('cancel-subscription-btn')).toBeEnabled();
|
||||
});
|
||||
await user.click(screen.getByTestId('cancel-subscription-btn'));
|
||||
|
||||
const input = screen.getByTestId('cancel-confirm-input');
|
||||
await user.type(input, 'cancel');
|
||||
@@ -107,9 +130,10 @@ describe('CancelSubscriptionBanner', () => {
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(),
|
||||
);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /cancel subscription/i }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('cancel-subscription-btn')).toBeEnabled();
|
||||
});
|
||||
await user.click(screen.getByTestId('cancel-subscription-btn'));
|
||||
expect(screen.getByTestId('cancel-confirm-input')).toHaveValue('');
|
||||
});
|
||||
|
||||
@@ -119,9 +143,10 @@ describe('CancelSubscriptionBanner', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<CancelSubscriptionBanner />);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /cancel subscription/i }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('cancel-subscription-btn')).toBeEnabled();
|
||||
});
|
||||
await user.click(screen.getByTestId('cancel-subscription-btn'));
|
||||
await user.type(screen.getByTestId('cancel-confirm-input'), 'cancel');
|
||||
await user.click(screen.getByTestId('cancel-subscription-confirm-btn'));
|
||||
|
||||
@@ -151,9 +176,10 @@ describe('CancelSubscriptionBanner', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<CancelSubscriptionBanner />);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /cancel subscription/i }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('cancel-subscription-btn')).toBeEnabled();
|
||||
});
|
||||
await user.click(screen.getByTestId('cancel-subscription-btn'));
|
||||
await user.type(screen.getByTestId('cancel-confirm-input'), 'cancel');
|
||||
await user.click(screen.getByTestId('cancel-subscription-confirm-btn'));
|
||||
|
||||
@@ -172,9 +198,10 @@ describe('CancelSubscriptionBanner', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<CancelSubscriptionBanner />);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /cancel subscription/i }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('cancel-subscription-btn')).toBeEnabled();
|
||||
});
|
||||
await user.click(screen.getByTestId('cancel-subscription-btn'));
|
||||
await user.type(screen.getByTestId('cancel-confirm-input'), 'cancel');
|
||||
await user.click(screen.getByTestId('cancel-subscription-confirm-btn'));
|
||||
|
||||
@@ -192,9 +219,10 @@ describe('CancelSubscriptionBanner', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<CancelSubscriptionBanner />);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /cancel subscription/i }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('cancel-subscription-btn')).toBeEnabled();
|
||||
});
|
||||
await user.click(screen.getByTestId('cancel-subscription-btn'));
|
||||
await user.type(screen.getByTestId('cancel-confirm-input'), 'cancel');
|
||||
await user.click(screen.getByTestId('cancel-subscription-confirm-btn'));
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
|
||||
import { SubscriptionDeletePermission } from 'lib/authz/hooks/useAuthZ/permissions/subscription.permissions';
|
||||
import { pick } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
@@ -178,15 +180,17 @@ function CancelSubscriptionBanner(): JSX.Element {
|
||||
immediately and removed from our servers.
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
<AuthZButton
|
||||
checks={[SubscriptionDeletePermission]}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
prefix={<X size={12} />}
|
||||
onClick={handleOpenCancelDialog}
|
||||
className={styles.cancelButton}
|
||||
testId="cancel-subscription-btn"
|
||||
>
|
||||
Cancel Subscription
|
||||
</Button>
|
||||
</AuthZButton>
|
||||
</div>
|
||||
<DialogWrapper
|
||||
open={dialogView !== null}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import {
|
||||
@@ -76,7 +75,7 @@ export function buildEntityMetricsChartConfig({
|
||||
show: true,
|
||||
side: 2,
|
||||
isDarkMode,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
isTimeAxis: true,
|
||||
});
|
||||
|
||||
builder.addAxis({
|
||||
@@ -85,7 +84,6 @@ export function buildEntityMetricsChartConfig({
|
||||
side: 3,
|
||||
isDarkMode,
|
||||
yAxisUnit,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
});
|
||||
|
||||
if (!apiResponse?.data?.result) {
|
||||
|
||||
@@ -333,6 +333,7 @@ describe('AttributeMappingsTab (integration)', () => {
|
||||
context: FieldContext.attribute,
|
||||
operation: MapperOperation.copy,
|
||||
priority,
|
||||
enabled: true,
|
||||
})),
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import { ConditionKey } from 'container/LLMObservability/AttributeMapping/types';
|
||||
|
||||
import styles from './ConditionsTooltip.module.scss';
|
||||
|
||||
interface ConditionsTooltipProps {
|
||||
attributes: string[];
|
||||
resource: string[];
|
||||
attributes: ConditionKey[];
|
||||
resource: ConditionKey[];
|
||||
}
|
||||
|
||||
function ConditionsTooltip({
|
||||
@@ -33,8 +35,8 @@ function ConditionsTooltip({
|
||||
</Typography.Text>
|
||||
<div className={styles.keyList}>
|
||||
{attributes.map((key) => (
|
||||
<code key={key} className={styles.key}>
|
||||
{key}
|
||||
<code key={`${key.origin}-${key.value}`} className={styles.key}>
|
||||
{key.value}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
@@ -47,8 +49,8 @@ function ConditionsTooltip({
|
||||
</Typography.Text>
|
||||
<div className={styles.keyList}>
|
||||
{resource.map((key) => (
|
||||
<code key={key} className={styles.key}>
|
||||
{key}
|
||||
<code key={`${key.origin}-${key.value}`} className={styles.key}>
|
||||
{key.value}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
SpantypesSpanMapperDTO as Mapper,
|
||||
SpantypesSpanMapperGroupDTO as MapperGroup,
|
||||
SpantypesSpanMapperOperationDTO as MapperOperation,
|
||||
SpantypesSpanMapperOriginDTO as MapperOrigin,
|
||||
SpantypesSpanMapperTestSpanDTO as TestSpan,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
@@ -21,9 +22,15 @@ export function makeGroup(overrides: Partial<MapperGroup> = {}): MapperGroup {
|
||||
orgId: 'org-1',
|
||||
name: 'demo',
|
||||
enabled: true,
|
||||
origin: MapperOrigin.user,
|
||||
version: 0,
|
||||
condition: {
|
||||
attributes: ['ai.embeddings'],
|
||||
resource: ['cloud.account.id'],
|
||||
attributes: [
|
||||
{ value: 'ai.embeddings', enabled: true, origin: MapperOrigin.user },
|
||||
],
|
||||
resource: [
|
||||
{ value: 'cloud.account.id', enabled: true, origin: MapperOrigin.user },
|
||||
],
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
@@ -35,6 +42,7 @@ export function makeMapper(overrides: Partial<Mapper> = {}): Mapper {
|
||||
groupId: 'group-1',
|
||||
name: 'gen_ai.request.model',
|
||||
enabled: true,
|
||||
origin: MapperOrigin.user,
|
||||
fieldContext: FieldContext.attribute,
|
||||
config: {
|
||||
sources: [
|
||||
@@ -43,12 +51,16 @@ export function makeMapper(overrides: Partial<Mapper> = {}): Mapper {
|
||||
context: FieldContext.attribute,
|
||||
operation: MapperOperation.copy,
|
||||
priority: 2,
|
||||
enabled: true,
|
||||
origin: MapperOrigin.user,
|
||||
},
|
||||
{
|
||||
key: 'llm.model',
|
||||
context: FieldContext.attribute,
|
||||
operation: MapperOperation.move,
|
||||
priority: 1,
|
||||
enabled: true,
|
||||
origin: MapperOrigin.user,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -85,8 +97,12 @@ export const mockGroups: MapperGroup[] = [
|
||||
id: 'group-1',
|
||||
name: 'demo',
|
||||
condition: {
|
||||
attributes: ['ai.embeddings'],
|
||||
resource: ['cloud.account.id'],
|
||||
attributes: [
|
||||
{ value: 'ai.embeddings', enabled: true, origin: MapperOrigin.user },
|
||||
],
|
||||
resource: [
|
||||
{ value: 'cloud.account.id', enabled: true, origin: MapperOrigin.user },
|
||||
],
|
||||
},
|
||||
}),
|
||||
makeGroup({
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Plus, X } from '@signozhq/icons';
|
||||
|
||||
import { FieldContextValue } from 'container/LLMObservability/AttributeMapping/types';
|
||||
import {
|
||||
ConditionKey,
|
||||
FieldContextValue,
|
||||
} from 'container/LLMObservability/AttributeMapping/types';
|
||||
import { createConditionKey } from 'container/LLMObservability/AttributeMapping/utils';
|
||||
import KeySearchInput from '../../../KeySearchInput/KeySearchInput';
|
||||
import styles from './ConditionKeyList.module.scss';
|
||||
|
||||
interface ConditionKeyListProps {
|
||||
label: string;
|
||||
labelHint?: string;
|
||||
keys: string[];
|
||||
keys: ConditionKey[];
|
||||
placeholder: string;
|
||||
addLabel: string;
|
||||
testIdPrefix: string;
|
||||
fieldContext: FieldContextValue;
|
||||
onChange: (keys: string[]) => void;
|
||||
onChange: (keys: ConditionKey[]) => void;
|
||||
}
|
||||
|
||||
function ConditionKeyList({
|
||||
@@ -27,11 +31,11 @@ function ConditionKeyList({
|
||||
onChange,
|
||||
}: ConditionKeyListProps): JSX.Element {
|
||||
const updateKey = (index: number, value: string): void => {
|
||||
onChange(keys.map((key, i) => (i === index ? value : key)));
|
||||
onChange(keys.map((key, i) => (i === index ? { ...key, value } : key)));
|
||||
};
|
||||
|
||||
const addKey = (): void => {
|
||||
onChange([...keys, '']);
|
||||
onChange([...keys, createConditionKey()]);
|
||||
};
|
||||
|
||||
const removeKey = (index: number): void => {
|
||||
@@ -53,7 +57,7 @@ function ConditionKeyList({
|
||||
<KeySearchInput
|
||||
className={styles.keyInput}
|
||||
placeholder={placeholder}
|
||||
value={key}
|
||||
value={key.value}
|
||||
fieldContext={fieldContext}
|
||||
onChange={(next): void => updateKey(index, next)}
|
||||
testId={`${testIdPrefix}-${index}`}
|
||||
|
||||
@@ -42,7 +42,9 @@ function sourcesEqual(a: SourceConfig[], b: SourceConfig[]): boolean {
|
||||
(source, index) =>
|
||||
source.key === b[index].key &&
|
||||
source.context === b[index].context &&
|
||||
source.operation === b[index].operation,
|
||||
source.operation === b[index].operation &&
|
||||
source.enabled === b[index].enabled &&
|
||||
source.origin === b[index].origin,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
SpantypesSpanMapperDTO,
|
||||
SpantypesSpanMapperGroupDTO,
|
||||
SpantypesSpanMapperOperationDTO,
|
||||
SpantypesSpanMapperOriginDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export type MapperGroup = SpantypesSpanMapperGroupDTO;
|
||||
@@ -11,6 +12,16 @@ export const FieldContext = SpantypesFieldContextDTO;
|
||||
export type FieldContextValue = SpantypesFieldContextDTO;
|
||||
export const MapperOperation = SpantypesSpanMapperOperationDTO;
|
||||
export type MapperOperationValue = SpantypesSpanMapperOperationDTO;
|
||||
export const MapperOrigin = SpantypesSpanMapperOriginDTO;
|
||||
export type MapperOriginValue = SpantypesSpanMapperOriginDTO;
|
||||
|
||||
// One condition substring. Shipped (system) keys are read-only apart from
|
||||
// `enabled`; user keys are fully editable.
|
||||
export interface ConditionKey {
|
||||
value: string;
|
||||
enabled: boolean;
|
||||
origin: MapperOriginValue;
|
||||
}
|
||||
|
||||
export type MapperDraftMode = 'add' | 'edit';
|
||||
|
||||
@@ -18,6 +29,8 @@ export interface SourceConfig {
|
||||
key: string;
|
||||
context: SpantypesFieldContextDTO;
|
||||
operation: SpantypesSpanMapperOperationDTO;
|
||||
enabled: boolean;
|
||||
origin: MapperOriginValue;
|
||||
}
|
||||
|
||||
// Editable form state for a mapper. `sources` is ordered highest priority
|
||||
@@ -33,8 +46,8 @@ export interface MapperDraft {
|
||||
export interface GroupDraft {
|
||||
id: string | null;
|
||||
name: string;
|
||||
attributes: string[];
|
||||
resource: string[];
|
||||
attributes: ConditionKey[];
|
||||
resource: ConditionKey[];
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
@@ -51,8 +64,8 @@ export interface DraftGroup {
|
||||
localId: string;
|
||||
serverId: string | null;
|
||||
name: string;
|
||||
attributes: string[];
|
||||
resource: string[];
|
||||
attributes: ConditionKey[];
|
||||
resource: ConditionKey[];
|
||||
enabled: boolean;
|
||||
mappers: DraftMapper[];
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import {
|
||||
SpantypesPostableSpanMapperDTO,
|
||||
SpantypesPostableSpanMapperGroupDTO,
|
||||
SpantypesSpanMapperGroupConditionKeyDTO,
|
||||
SpantypesUpdatableSpanMapperDTO,
|
||||
SpantypesUpdatableSpanMapperGroupDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import {
|
||||
ConditionKey,
|
||||
DraftGroup,
|
||||
DraftMapper,
|
||||
FieldContext,
|
||||
@@ -15,6 +17,7 @@ import {
|
||||
MapperDraft,
|
||||
MapperGroup,
|
||||
MapperOperation,
|
||||
MapperOrigin,
|
||||
SourceConfig,
|
||||
} from './types';
|
||||
|
||||
@@ -24,20 +27,36 @@ function genLocalId(prefix: 'group' | 'mapper'): string {
|
||||
return `local-${prefix}-${uuid()}`;
|
||||
}
|
||||
|
||||
// Trimmed, de-duplicated, non-empty keys preserving input order.
|
||||
function cleanKeys(keys: string[]): string[] {
|
||||
export function createConditionKey(value = ''): ConditionKey {
|
||||
return { value, enabled: true, origin: MapperOrigin.user };
|
||||
}
|
||||
|
||||
// Trimmed, de-duplicated, non-empty keys preserving input order. A shipped and
|
||||
// a user key may share a value, so the origin is part of the identity.
|
||||
function cleanKeys(keys: ConditionKey[]): ConditionKey[] {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
const result: ConditionKey[] = [];
|
||||
keys.forEach((raw) => {
|
||||
const key = raw.trim();
|
||||
if (key && !seen.has(key)) {
|
||||
seen.add(key);
|
||||
result.push(key);
|
||||
const value = raw.value.trim();
|
||||
const dedupeKey = `${raw.origin}:${value}`;
|
||||
if (value && !seen.has(dedupeKey)) {
|
||||
seen.add(dedupeKey);
|
||||
result.push({ ...raw, value });
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function fromConditionKeys(
|
||||
keys: SpantypesSpanMapperGroupConditionKeyDTO[] | null | undefined,
|
||||
): ConditionKey[] {
|
||||
return (keys ?? []).map((key) => ({
|
||||
value: key.value,
|
||||
enabled: key.enabled,
|
||||
origin: key.origin ?? MapperOrigin.user,
|
||||
}));
|
||||
}
|
||||
|
||||
// Source configs for a mapper, highest priority first (first match wins at
|
||||
// evaluation time).
|
||||
function getMapperSources(mapper: Mapper): SourceConfig[] {
|
||||
@@ -48,6 +67,8 @@ function getMapperSources(mapper: Mapper): SourceConfig[] {
|
||||
key: source.key,
|
||||
context: source.context,
|
||||
operation: source.operation,
|
||||
enabled: source.enabled,
|
||||
origin: source.origin ?? MapperOrigin.user,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -56,6 +77,8 @@ export function createEmptySource(): SourceConfig {
|
||||
key: '',
|
||||
context: FieldContext.attribute,
|
||||
operation: MapperOperation.copy,
|
||||
enabled: true,
|
||||
origin: MapperOrigin.user,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,7 +95,7 @@ function getCleanSources(draft: MapperDraft): SourceConfig[] {
|
||||
const result: SourceConfig[] = [];
|
||||
draft.sources.forEach((source) => {
|
||||
const key = source.key.trim();
|
||||
const dedupeKey = `${source.context}:${key}`;
|
||||
const dedupeKey = `${source.origin}:${source.context}:${key}`;
|
||||
if (key && !seen.has(dedupeKey)) {
|
||||
seen.add(dedupeKey);
|
||||
result.push({ ...source, key });
|
||||
@@ -95,6 +118,8 @@ function buildSources(
|
||||
context: source.context,
|
||||
operation: source.operation,
|
||||
priority: sources.length - index,
|
||||
enabled: source.enabled,
|
||||
origin: source.origin,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -123,7 +148,7 @@ export function buildUpdatableMapper(
|
||||
export const EMPTY_GROUP_DRAFT: GroupDraft = {
|
||||
id: null,
|
||||
name: '',
|
||||
attributes: [''],
|
||||
attributes: [createConditionKey()],
|
||||
resource: [],
|
||||
enabled: true,
|
||||
};
|
||||
@@ -170,8 +195,8 @@ export function buildDraftGroup(
|
||||
localId: group.id,
|
||||
serverId: group.id,
|
||||
name: group.name,
|
||||
attributes: group.condition?.attributes ?? [],
|
||||
resource: group.condition?.resource ?? [],
|
||||
attributes: fromConditionKeys(group.condition?.attributes),
|
||||
resource: fromConditionKeys(group.condition?.resource),
|
||||
enabled: group.enabled,
|
||||
mappers: mappers.map(buildDraftMapper),
|
||||
};
|
||||
@@ -182,7 +207,8 @@ export function groupDraftFromNode(group: DraftGroup): GroupDraft {
|
||||
return {
|
||||
id: group.localId,
|
||||
name: group.name,
|
||||
attributes: group.attributes.length > 0 ? group.attributes : [''],
|
||||
attributes:
|
||||
group.attributes.length > 0 ? group.attributes : [createConditionKey()],
|
||||
resource: group.resource,
|
||||
enabled: group.enabled,
|
||||
};
|
||||
|
||||
@@ -11,17 +11,13 @@ import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { initialQueryAIWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { usePageActions } from 'container/AIAssistant/pageActions/usePageActions';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import Toolbar from 'container/Toolbar/Toolbar';
|
||||
import {
|
||||
getExportQueryData,
|
||||
getQueryByPanelType,
|
||||
} from 'container/TracesExplorer/explorerUtils';
|
||||
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
|
||||
import { useGetExportToDashboardLink } from 'hooks/dashboard/useGetExportToDashboardLink';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
@@ -52,6 +48,7 @@ import {
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { TOOLBAR_VIEWS } from './constants';
|
||||
import { getExportQueryData, getQueryByPanelType } from './explorerUtils';
|
||||
import ListView from './ListView/ListView';
|
||||
import { defaultSelectedColumns } from './ListView/configs';
|
||||
import QuerySection from './QuerySection/QuerySection';
|
||||
@@ -118,7 +115,7 @@ function Explorer(): JSX.Element {
|
||||
const defaultQuery = useMemo(
|
||||
(): Query =>
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap.traces,
|
||||
initialQueryAIWithType,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.TRACES,
|
||||
),
|
||||
@@ -185,7 +182,7 @@ function Explorer(): JSX.Element {
|
||||
const exportDefaultQuery = useMemo(
|
||||
() =>
|
||||
getQueryByPanelType(
|
||||
stagedQuery || initialQueriesMap.traces,
|
||||
stagedQuery || initialQueryAIWithType,
|
||||
panelType || PANEL_TYPES.LIST,
|
||||
),
|
||||
[stagedQuery, panelType],
|
||||
|
||||
@@ -17,12 +17,11 @@ import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { initialQueryAIWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import TraceExplorerControls from 'container/TracesExplorer/Controls';
|
||||
import { getListViewQuery } from 'container/TracesExplorer/explorerUtils';
|
||||
import {
|
||||
getTraceLink,
|
||||
transformSpanRows,
|
||||
@@ -43,6 +42,7 @@ import { Warning } from 'types/api';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { getListViewQuery } from '../explorerUtils';
|
||||
import {
|
||||
defaultSelectedColumns,
|
||||
PER_PAGE_OPTIONS,
|
||||
@@ -94,7 +94,7 @@ function ListView({
|
||||
paginationQueryData ?? getDefaultPaginationConfig(PER_PAGE_OPTIONS);
|
||||
|
||||
const requestQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueriesMap.traces, orderBy),
|
||||
() => getListViewQuery(stagedQuery || initialQueryAIWithType, orderBy),
|
||||
[stagedQuery, orderBy],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ExplorerOrderBy from 'container/ExplorerOrderBy';
|
||||
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -10,33 +8,16 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
function QuerySection(): JSX.Element {
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
|
||||
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||
const isList = panelTypes === PANEL_TYPES.LIST;
|
||||
const config: QueryBuilderProps['filterConfigs'] = {
|
||||
// Only reaches the builder for timeseries/table; list/trace panels use QueryBuilderV2's listViewTracesFilterConfigs.
|
||||
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(
|
||||
() => ({
|
||||
stepInterval: { isHidden: false, isDisabled: false },
|
||||
limit: { isHidden: isList, isDisabled: true },
|
||||
having: { isHidden: isList, isDisabled: true },
|
||||
};
|
||||
|
||||
return config;
|
||||
}, [panelTypes]);
|
||||
|
||||
const renderOrderBy = useCallback(
|
||||
({ query, onChange }: OrderByFilterProps) => (
|
||||
<ExplorerOrderBy query={query} onChange={onChange} />
|
||||
),
|
||||
limit: { isHidden: false, isDisabled: true },
|
||||
having: { isHidden: false, isDisabled: true },
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const queryComponents = useMemo((): QueryBuilderProps['queryComponents'] => {
|
||||
const shouldRenderCustomOrderBy =
|
||||
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE;
|
||||
|
||||
return {
|
||||
...(shouldRenderCustomOrderBy ? { renderOrderBy } : {}),
|
||||
};
|
||||
}, [panelTypes, renderOrderBy]);
|
||||
|
||||
const isListViewPanel = useMemo(
|
||||
() => panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE,
|
||||
[panelTypes],
|
||||
@@ -45,14 +26,10 @@ function QuerySection(): JSX.Element {
|
||||
return (
|
||||
<QueryBuilderV2
|
||||
isListViewPanel={isListViewPanel}
|
||||
showTraceOperator
|
||||
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||
queryComponents={queryComponents}
|
||||
panelType={panelTypes}
|
||||
filterConfigs={filterConfigs}
|
||||
showOnlyWhereClause={
|
||||
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE
|
||||
}
|
||||
showOnlyWhereClause={isListViewPanel}
|
||||
version="v3" // setting this to v3 as we this is rendered in logs explorer
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -14,10 +14,9 @@ import logEvent from 'api/common/logEvent';
|
||||
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { initialQueryAIWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import TraceExplorerControls from 'container/TracesExplorer/Controls';
|
||||
import { getListViewQuery } from 'container/TracesExplorer/explorerUtils';
|
||||
import { getTraceLink } from 'container/TracesExplorer/ListView/utils';
|
||||
import { TracesTableRow } from 'container/TracesExplorer/TracesTable/getFieldColumn';
|
||||
import TracesTable from 'container/TracesExplorer/TracesTable/TracesTable';
|
||||
@@ -31,6 +30,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import DOCLINKS from 'utils/docLinks';
|
||||
|
||||
import { getListViewQuery } from '../explorerUtils';
|
||||
import { columns, PER_PAGE_OPTIONS } from './configs';
|
||||
import styles from './TracesView.module.scss';
|
||||
|
||||
@@ -60,7 +60,7 @@ function TracesView({
|
||||
);
|
||||
|
||||
const transformedQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueriesMap.traces),
|
||||
() => getListViewQuery(stagedQuery || initialQueryAIWithType),
|
||||
[stagedQuery],
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { cloneDeep, set } from 'lodash-es';
|
||||
import { OrderByPayload, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const getListViewQuery = (
|
||||
stagedQuery: Query,
|
||||
orderBy?: string,
|
||||
): Query => {
|
||||
const query = stagedQuery
|
||||
? cloneDeep(stagedQuery)
|
||||
: cloneDeep(initialQueriesMap.traces);
|
||||
|
||||
const orderByPayload: OrderByPayload[] = orderBy
|
||||
? [
|
||||
{
|
||||
columnName: orderBy.split(':')[0],
|
||||
order: orderBy.split(':')[1] as 'asc' | 'desc',
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
for (let i = 0; i < query.builder.queryData.length; i++) {
|
||||
const queryData = query.builder.queryData[i];
|
||||
queryData.groupBy = [];
|
||||
queryData.having = {
|
||||
expression: '',
|
||||
};
|
||||
queryData.orderBy = orderByPayload;
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
export const getQueryByPanelType = (
|
||||
stagedQuery: Query,
|
||||
panelType: PANEL_TYPES,
|
||||
): Query => {
|
||||
if (panelType === PANEL_TYPES.LIST || panelType === PANEL_TYPES.TRACE) {
|
||||
return getListViewQuery(stagedQuery);
|
||||
}
|
||||
return stagedQuery;
|
||||
};
|
||||
|
||||
export const getExportQueryData = (
|
||||
query: Query,
|
||||
panelType: PANEL_TYPES,
|
||||
options: OptionsQuery,
|
||||
): Query => {
|
||||
if (panelType === PANEL_TYPES.LIST) {
|
||||
const updatedQuery = cloneDeep(query);
|
||||
set(
|
||||
updatedQuery,
|
||||
'builder.queryData[0].selectColumns',
|
||||
options.selectColumns,
|
||||
);
|
||||
|
||||
return updatedQuery;
|
||||
}
|
||||
return query;
|
||||
};
|
||||
@@ -108,7 +108,6 @@ function LogsExplorerViewsContainer({
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [logs, setLogs] = useState<ILog[]>([]);
|
||||
const [requestData, setRequestData] = useState<Query | null>(null);
|
||||
const [queryId, setQueryId] = useState<string>(v4());
|
||||
const [listChartQuery, setListChartQuery] = useState<Query | null>(null);
|
||||
|
||||
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
|
||||
@@ -180,12 +179,7 @@ function LogsExplorerViewsContainer({
|
||||
},
|
||||
undefined,
|
||||
listQueryKeyRef,
|
||||
{
|
||||
...(!isEmpty(queryId) &&
|
||||
selectedPanelType !== PANEL_TYPES.LIST && {
|
||||
'X-SIGNOZ-QUERY-ID': queryId,
|
||||
}),
|
||||
},
|
||||
undefined,
|
||||
// custom selected time interval to prevent recalculating the start and end timestamps before fetching next pages
|
||||
'custom',
|
||||
);
|
||||
@@ -250,10 +244,6 @@ function LogsExplorerViewsContainer({
|
||||
setRequestData(newRequestData);
|
||||
}, [isLimit, logs, listQuery, pageSize, stagedQuery, getRequestData, page]);
|
||||
|
||||
useEffect(() => {
|
||||
setQueryId(v4());
|
||||
}, [data]);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current && !isUndefined(data?.payload)) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import {
|
||||
@@ -72,7 +71,7 @@ export function buildMeterChartConfig({
|
||||
show: true,
|
||||
side: 2,
|
||||
isDarkMode,
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
isTimeAxis: true,
|
||||
});
|
||||
|
||||
builder.addAxis({
|
||||
@@ -81,7 +80,6 @@ export function buildMeterChartConfig({
|
||||
side: 3,
|
||||
isDarkMode,
|
||||
yAxisUnit,
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
});
|
||||
|
||||
if (!apiResponse?.data?.result) {
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||
import type { WhereClauseConfig } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
|
||||
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
|
||||
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||
@@ -263,10 +263,7 @@ function QueryBuilderSearchV2(
|
||||
return false;
|
||||
}, [currentState, query.aggregateAttribute?.dataType, query.dataSource]);
|
||||
|
||||
const dashboardDynamicVariables = useDashboardVariablesByType(
|
||||
'DYNAMIC',
|
||||
'values',
|
||||
);
|
||||
const dashboardDynamicVariables = useDynamicVariableSuggestions();
|
||||
|
||||
const { data, isFetching } = useGetAggregateKeys(
|
||||
{
|
||||
@@ -816,9 +813,8 @@ function QueryBuilderSearchV2(
|
||||
values.push(...(attributeValues?.payload?.[key] || []));
|
||||
|
||||
// here we want to suggest the variable name matching with the key here, we will go over the dynamic variables for the keys
|
||||
const variableName = dashboardDynamicVariables?.find(
|
||||
(variable) =>
|
||||
variable?.dynamicVariablesAttribute === currentFilterItem?.key?.key,
|
||||
const variableName = dashboardDynamicVariables.find(
|
||||
(variable) => variable.attribute === currentFilterItem?.key?.key,
|
||||
)?.name;
|
||||
|
||||
if (variableName) {
|
||||
|
||||
@@ -5,9 +5,8 @@ import {
|
||||
initialQueriesMap,
|
||||
initialQueryBuilderFormValues,
|
||||
} from 'constants/queryBuilder';
|
||||
import { IUseDashboardVariablesReturn } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { DynamicVariableSuggestion } from 'providers/Dashboard/store/dynamicVariableSuggestions';
|
||||
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/variables';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -150,24 +149,14 @@ jest.mock('hooks/useSafeNavigate', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock dashboard variables
|
||||
const dashboardVariables = {
|
||||
service: {
|
||||
id: 'service',
|
||||
name: 'service',
|
||||
type: 'DYNAMIC' as IDashboardVariable['type'],
|
||||
dynamicVariablesAttribute: 'service.name',
|
||||
description: '',
|
||||
sort: 'DISABLED' as IDashboardVariable['sort'],
|
||||
multiSelect: false,
|
||||
showALLOption: false,
|
||||
},
|
||||
};
|
||||
// Mock the dynamic variables the open dashboard would publish
|
||||
const dynamicVariableSuggestions = [
|
||||
{ name: 'service', attribute: 'service.name' },
|
||||
];
|
||||
|
||||
jest.mock('hooks/dashboard/useDashboardVariables', () => ({
|
||||
useDashboardVariables: (): IUseDashboardVariablesReturn => ({
|
||||
dashboardVariables: dashboardVariables,
|
||||
}),
|
||||
jest.mock('hooks/dashboard/useDynamicVariableSuggestions', () => ({
|
||||
useDynamicVariableSuggestions: (): DynamicVariableSuggestion[] =>
|
||||
dynamicVariableSuggestions,
|
||||
}));
|
||||
|
||||
describe('Suggestion Key -> Operator -> Value Flow', () => {
|
||||
|
||||
@@ -329,16 +329,17 @@ describe('transformTransactionGroupsToResourcePermissions', () => {
|
||||
it('returns all resources from RESOURCE_ORDER even with empty transaction groups', () => {
|
||||
const result = transformTransactionGroupsToResourcePermissions([]);
|
||||
|
||||
expect(result).toHaveLength(8);
|
||||
expect(result).toHaveLength(9);
|
||||
expect(result.map((r) => r.resourceKind)).toStrictEqual([
|
||||
'factor-api-key',
|
||||
'role',
|
||||
'serviceaccount',
|
||||
'license',
|
||||
'logs',
|
||||
'traces',
|
||||
'metrics',
|
||||
'meter-metrics',
|
||||
'metrics',
|
||||
'role',
|
||||
'serviceaccount',
|
||||
'subscription',
|
||||
'traces',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -419,16 +420,17 @@ describe('createEmptyRolePermissions', () => {
|
||||
it('creates permissions for all resources in RESOURCE_ORDER', () => {
|
||||
const result = createEmptyRolePermissions();
|
||||
|
||||
expect(result).toHaveLength(8);
|
||||
expect(result).toHaveLength(9);
|
||||
expect(result.map((r) => r.resourceKind)).toStrictEqual([
|
||||
'factor-api-key',
|
||||
'role',
|
||||
'serviceaccount',
|
||||
'license',
|
||||
'logs',
|
||||
'traces',
|
||||
'metrics',
|
||||
'meter-metrics',
|
||||
'metrics',
|
||||
'role',
|
||||
'serviceaccount',
|
||||
'subscription',
|
||||
'traces',
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Gauge,
|
||||
Key,
|
||||
Logs,
|
||||
Receipt,
|
||||
Shield,
|
||||
} from '@signozhq/icons';
|
||||
|
||||
@@ -69,6 +70,13 @@ export const RESOURCE_PANELS: Record<AuthZResource, ResourcePanelConfig> = {
|
||||
selectorPlaceholder: 'Type license ID, separate multiple with comma or space',
|
||||
docsAnchor: 'license',
|
||||
},
|
||||
subscription: {
|
||||
label: 'Subscription',
|
||||
description: 'The workspace subscription, its usage and billing details.',
|
||||
icon: Receipt,
|
||||
selectorPlaceholder: 'Type * to cover the workspace subscription',
|
||||
docsAnchor: 'subscription',
|
||||
},
|
||||
logs: {
|
||||
label: 'Logs',
|
||||
description: 'Log data collected across the workspace.',
|
||||
@@ -107,7 +115,11 @@ export const RESOURCE_PANELS: Record<AuthZResource, ResourcePanelConfig> = {
|
||||
},
|
||||
};
|
||||
|
||||
export const RESOURCE_ORDER = Object.keys(RESOURCE_PANELS) as AuthZResource[];
|
||||
export const RESOURCE_ORDER = (
|
||||
Object.keys(RESOURCE_PANELS) as AuthZResource[]
|
||||
).sort((left, right) =>
|
||||
RESOURCE_PANELS[left].label.localeCompare(RESOURCE_PANELS[right].label),
|
||||
);
|
||||
|
||||
export function getResourcePanel(resource: AuthZResource): ResourcePanelConfig {
|
||||
const panel = RESOURCE_PANELS[resource];
|
||||
|
||||
@@ -2,7 +2,6 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
|
||||
import { ServicesList } from 'types/api/metrics/getService';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
@@ -47,7 +46,6 @@ export const getQueryRangeRequestData = ({
|
||||
graphType: serviceMetricsWidget?.panelTypes,
|
||||
query: updatedQuery,
|
||||
globalSelectedInterval,
|
||||
variables: getDashboardVariables(),
|
||||
});
|
||||
});
|
||||
return requestData;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useAutoRefreshSelection } from './useAutoRefreshSelection';
|
||||
import { useAutoRefreshTick } from './useAutoRefreshTick';
|
||||
|
||||
/** Auto-refresh timer for views that hide the time selector that normally owns it. */
|
||||
function AutoRefreshTicker(): null {
|
||||
const { isEnabled, intervalMs } = useAutoRefreshSelection();
|
||||
|
||||
useAutoRefreshTick(isEnabled, intervalMs);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default AutoRefreshTicker;
|
||||
@@ -0,0 +1,116 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import set from 'api/browser/localstorage/set';
|
||||
import { DASHBOARD_TIME_IN_DURATION } from 'constants/app';
|
||||
import configureStore, { MockStoreEnhanced } from 'redux-mock-store';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import AutoRefresh from '../index';
|
||||
|
||||
const mockStore = configureStore<Partial<AppState>>([]);
|
||||
|
||||
const PATHNAME = '/dashboard/test-id';
|
||||
const randomTime = 1700000000000000000;
|
||||
|
||||
function createGlobalTimeState(
|
||||
overrides: Partial<GlobalReducer> = {},
|
||||
): GlobalReducer {
|
||||
return {
|
||||
minTime: randomTime,
|
||||
maxTime: randomTime,
|
||||
loading: false,
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: false,
|
||||
selectedAutoRefreshInterval: '5s',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function renderAutoRefresh(
|
||||
globalTime: GlobalReducer,
|
||||
props: { disabled?: boolean } = {},
|
||||
): MockStoreEnhanced<Partial<AppState>> {
|
||||
const store = mockStore({ globalTime });
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={[PATHNAME]}>
|
||||
<Provider store={store}>
|
||||
<AutoRefresh {...props} />
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
function tickCount(store: MockStoreEnhanced<Partial<AppState>>): number {
|
||||
return store.getActions().filter((a) => a.type === UPDATE_TIME_INTERVAL)
|
||||
.length;
|
||||
}
|
||||
|
||||
describe('AutoRefresh', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('renders the trigger and ticks on the persisted interval', () => {
|
||||
set(DASHBOARD_TIME_IN_DURATION, JSON.stringify({ [PATHNAME]: '5s' }));
|
||||
|
||||
const store = renderAutoRefresh(createGlobalTimeState());
|
||||
|
||||
expect(screen.getByTitle('Set auto refresh')).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(15_000);
|
||||
});
|
||||
|
||||
expect(tickCount(store)).toBe(3);
|
||||
});
|
||||
|
||||
it('does not tick when auto refresh was never enabled for the route', () => {
|
||||
const store = renderAutoRefresh(createGlobalTimeState());
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(60_000);
|
||||
});
|
||||
|
||||
expect(tickCount(store)).toBe(0);
|
||||
});
|
||||
|
||||
it('does not tick while the disabled prop is set', () => {
|
||||
set(DASHBOARD_TIME_IN_DURATION, JSON.stringify({ [PATHNAME]: '5s' }));
|
||||
|
||||
const store = renderAutoRefresh(createGlobalTimeState(), { disabled: true });
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(60_000);
|
||||
});
|
||||
|
||||
expect(tickCount(store)).toBe(0);
|
||||
});
|
||||
|
||||
it('renders nothing on a custom time range', () => {
|
||||
set(DASHBOARD_TIME_IN_DURATION, JSON.stringify({ [PATHNAME]: '5s' }));
|
||||
|
||||
const store = renderAutoRefresh(
|
||||
createGlobalTimeState({ selectedTime: 'custom' }),
|
||||
);
|
||||
|
||||
expect(screen.queryByTitle('Set auto refresh')).not.toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(60_000);
|
||||
});
|
||||
|
||||
expect(tickCount(store)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import set from 'api/browser/localstorage/set';
|
||||
import { DASHBOARD_TIME_IN_DURATION } from 'constants/app';
|
||||
import configureStore, { MockStoreEnhanced } from 'redux-mock-store';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import AutoRefresh from '../index';
|
||||
import AutoRefreshTicker from '../AutoRefreshTicker';
|
||||
|
||||
const mockStore = configureStore<Partial<AppState>>([]);
|
||||
|
||||
const PATHNAME = '/dashboard/test-id';
|
||||
const randomTime = 1700000000000000000;
|
||||
|
||||
function createGlobalTimeState(
|
||||
overrides: Partial<GlobalReducer> = {},
|
||||
): GlobalReducer {
|
||||
return {
|
||||
minTime: randomTime,
|
||||
maxTime: randomTime,
|
||||
loading: false,
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: false,
|
||||
selectedAutoRefreshInterval: '5s',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function renderTicker(
|
||||
globalTime: GlobalReducer,
|
||||
): MockStoreEnhanced<Partial<AppState>> {
|
||||
const store = mockStore({ globalTime });
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={[PATHNAME]}>
|
||||
<Provider store={store}>
|
||||
<AutoRefreshTicker />
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
function timeIntervalActions(
|
||||
store: MockStoreEnhanced<Partial<AppState>>,
|
||||
): unknown[] {
|
||||
return store.getActions().filter((a) => a.type === UPDATE_TIME_INTERVAL);
|
||||
}
|
||||
|
||||
describe('AutoRefreshTicker', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('advances the global time window on the interval persisted for the route', () => {
|
||||
set(DASHBOARD_TIME_IN_DURATION, JSON.stringify({ [PATHNAME]: '5s' }));
|
||||
|
||||
const store = renderTicker(createGlobalTimeState());
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(10_000);
|
||||
});
|
||||
|
||||
expect(timeIntervalActions(store)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('does not tick when the route has no persisted interval', () => {
|
||||
const store = renderTicker(createGlobalTimeState());
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(60_000);
|
||||
});
|
||||
|
||||
expect(timeIntervalActions(store)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not tick while auto refresh is globally disabled', () => {
|
||||
set(DASHBOARD_TIME_IN_DURATION, JSON.stringify({ [PATHNAME]: '5s' }));
|
||||
|
||||
const store = renderTicker(
|
||||
createGlobalTimeState({ isAutoRefreshDisabled: true }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(60_000);
|
||||
});
|
||||
|
||||
expect(timeIntervalActions(store)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not tick on a custom time range', () => {
|
||||
set(DASHBOARD_TIME_IN_DURATION, JSON.stringify({ [PATHNAME]: '5s' }));
|
||||
|
||||
const store = renderTicker(createGlobalTimeState({ selectedTime: 'custom' }));
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(60_000);
|
||||
});
|
||||
|
||||
expect(timeIntervalActions(store)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Mirrors DashboardContainer's swap: exactly one of the two must be ticking.
|
||||
describe('AutoRefresh full screen handover', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('keeps a single timer running across entering and leaving full screen', () => {
|
||||
set(DASHBOARD_TIME_IN_DURATION, JSON.stringify({ [PATHNAME]: '5s' }));
|
||||
|
||||
const store = mockStore({ globalTime: createGlobalTimeState() });
|
||||
|
||||
function Harness({ active }: { active: boolean }): JSX.Element {
|
||||
return active ? <AutoRefreshTicker /> : <AutoRefresh />;
|
||||
}
|
||||
|
||||
const renderHarness = (active: boolean): JSX.Element => (
|
||||
<MemoryRouter initialEntries={[PATHNAME]}>
|
||||
<Provider store={store}>
|
||||
<Harness active={active} />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const { rerender } = render(renderHarness(false));
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(10_000);
|
||||
});
|
||||
expect(timeIntervalActions(store)).toHaveLength(2);
|
||||
|
||||
rerender(renderHarness(true));
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(10_000);
|
||||
});
|
||||
expect(timeIntervalActions(store)).toHaveLength(4);
|
||||
|
||||
rerender(renderHarness(false));
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(10_000);
|
||||
});
|
||||
expect(timeIntervalActions(store)).toHaveLength(6);
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useInterval } from 'react-use';
|
||||
import { Check, ChevronDown } from '@signozhq/icons';
|
||||
import { Button, Popover } from 'antd';
|
||||
import { Checkbox } from '@signozhq/ui/checkbox';
|
||||
@@ -11,21 +10,18 @@ import get from 'api/browser/localstorage/get';
|
||||
import set from 'api/browser/localstorage/set';
|
||||
import { DASHBOARD_TIME_IN_DURATION } from 'constants/app';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||
import _omit from 'lodash-es/omit';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
UPDATE_AUTO_REFRESH_INTERVAL,
|
||||
UPDATE_TIME_INTERVAL,
|
||||
} from 'types/actions/globalTime';
|
||||
import { UPDATE_AUTO_REFRESH_INTERVAL } from 'types/actions/globalTime';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { refreshIntervalOptions } from './constants';
|
||||
import { ButtonContainer } from './styles';
|
||||
import { useAutoRefreshTick } from './useAutoRefreshTick';
|
||||
|
||||
import './AutoRefreshV2.styles.scss';
|
||||
|
||||
@@ -93,30 +89,10 @@ function AutoRefresh({
|
||||
[selectedOption],
|
||||
);
|
||||
|
||||
useInterval(() => {
|
||||
const selectedValue = getOption?.value;
|
||||
|
||||
if (isDisabled || !isAutoRefreshEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedOption !== 'off' && selectedValue) {
|
||||
const { maxTime, minTime } = getMinMaxForSelectedTime(
|
||||
globalTime.selectedTime,
|
||||
globalTime.minTime,
|
||||
globalTime.maxTime,
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_TIME_INTERVAL,
|
||||
payload: {
|
||||
maxTime,
|
||||
minTime,
|
||||
selectedTime: globalTime.selectedTime,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, getOption?.value || 0);
|
||||
useAutoRefreshTick(
|
||||
!isDisabled && isAutoRefreshEnabled && selectedOption !== 'off',
|
||||
getOption?.value || 0,
|
||||
);
|
||||
|
||||
const onChangeHandler = useCallback(
|
||||
(selectedValue: string) => {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import get from 'api/browser/localstorage/get';
|
||||
import { DASHBOARD_TIME_IN_DURATION } from 'constants/app';
|
||||
|
||||
import { refreshIntervalOptions } from './constants';
|
||||
|
||||
export interface AutoRefreshSelection {
|
||||
isEnabled: boolean;
|
||||
intervalMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* An entry for the current route means auto-refresh is on, its absence means off.
|
||||
* Read on every render because localStorage isn't reactive.
|
||||
*/
|
||||
export function useAutoRefreshSelection(): AutoRefreshSelection {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const selectedOption = JSON.parse(get(DASHBOARD_TIME_IN_DURATION) || '{}')[
|
||||
pathname
|
||||
];
|
||||
|
||||
return {
|
||||
isEnabled: Boolean(selectedOption),
|
||||
intervalMs:
|
||||
refreshIntervalOptions.find((option) => option.key === selectedOption)
|
||||
?.value || 0,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useInterval } from 'react-use';
|
||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
/**
|
||||
* Advances the global time window on the auto-refresh interval. The global
|
||||
* "auto refresh disabled" flag and a custom range override the caller's `enabled`.
|
||||
*/
|
||||
export function useAutoRefreshTick(enabled: boolean, intervalMs: number): void {
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const isTicking =
|
||||
enabled &&
|
||||
intervalMs > 0 &&
|
||||
!globalTime.isAutoRefreshDisabled &&
|
||||
globalTime.selectedTime !== 'custom';
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
const { maxTime, minTime } = getMinMaxForSelectedTime(
|
||||
globalTime.selectedTime,
|
||||
globalTime.minTime,
|
||||
globalTime.maxTime,
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_TIME_INTERVAL,
|
||||
payload: {
|
||||
maxTime,
|
||||
minTime,
|
||||
selectedTime: globalTime.selectedTime,
|
||||
},
|
||||
});
|
||||
},
|
||||
isTicking ? intervalMs : null,
|
||||
);
|
||||
}
|
||||
@@ -28,13 +28,11 @@ import { populateMultipleResults } from 'lib/query/populateMultipleResults';
|
||||
import { timeItems, timePreferance } from 'constants/timePreference';
|
||||
import PanelWrapper from 'container/WidgetCard/Panels/PanelWrapper';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useChartMutable } from 'hooks/useChartMutable';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -82,8 +80,6 @@ function FullView({
|
||||
setCurrentGraphRef(fullViewRef);
|
||||
}, [setCurrentGraphRef]);
|
||||
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
|
||||
const getSelectedTime = useCallback(
|
||||
() =>
|
||||
timeItems.find((e) => e.enum === (widget?.timePreferance || 'GLOBAL_TIME')),
|
||||
@@ -115,7 +111,6 @@ function FullView({
|
||||
graphType: getGraphType(selectedPanelType),
|
||||
query: updatedQuery,
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
variables: getDashboardVariables(dashboardVariables),
|
||||
fillGaps: widget.fillSpans,
|
||||
formatForWeb: selectedPanelType === PANEL_TYPES.TABLE,
|
||||
originalGraphType: selectedPanelType,
|
||||
@@ -126,7 +121,6 @@ function FullView({
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: widget?.timePreferance || 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
variables: getDashboardVariables(dashboardVariables),
|
||||
tableParams: {
|
||||
pagination: {
|
||||
offset: 0,
|
||||
|
||||
@@ -8,12 +8,9 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useScrollWidgetIntoView } from 'lib/visualization/hooks/useScrollWidgetIntoView';
|
||||
import { populateMultipleResults } from 'lib/query/populateMultipleResults';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsPanelWaitingOnVariable } from 'hooks/dashboard/useVariableFetchState';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
|
||||
import { getVariableReferencesInQuery } from 'lib/dashboardVariables/variableReference';
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
@@ -45,7 +42,6 @@ function GridCardGraph({
|
||||
headerMenuList = [MenuItemKeys.View],
|
||||
isQueryEnabled,
|
||||
threshold,
|
||||
variables,
|
||||
version,
|
||||
onClickHandler,
|
||||
onDragSelect,
|
||||
@@ -113,25 +109,10 @@ function GridCardGraph({
|
||||
|
||||
const updatedQuery = widget?.query;
|
||||
|
||||
const referencedVariableNames = useMemo(() => {
|
||||
if (!variables || !updatedQuery) {
|
||||
return [];
|
||||
}
|
||||
const allNames = Object.values(variables)
|
||||
.map((v) => v.name)
|
||||
.filter((name): name is string => !!name);
|
||||
return getVariableReferencesInQuery(updatedQuery, allNames);
|
||||
}, [updatedQuery, variables]);
|
||||
|
||||
const isEmptyWidget =
|
||||
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
|
||||
|
||||
const isPanelWaitingOnAnyVariable = useIsPanelWaitingOnVariable(
|
||||
referencedVariableNames,
|
||||
);
|
||||
|
||||
const queryEnabledCondition =
|
||||
isVisible && !isEmptyWidget && isQueryEnabled && !isPanelWaitingOnAnyVariable;
|
||||
const queryEnabledCondition = isVisible && !isEmptyWidget && isQueryEnabled;
|
||||
|
||||
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
|
||||
if (widget.panelTypes !== PANEL_TYPES.LIST) {
|
||||
@@ -140,7 +121,6 @@ function GridCardGraph({
|
||||
graphType: getGraphType(widget.panelTypes),
|
||||
query: updatedQuery,
|
||||
globalSelectedInterval,
|
||||
variables: getDashboardVariables(variables),
|
||||
fillGaps: widget.fillSpans,
|
||||
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
|
||||
start: customTimeRange?.startTime || start,
|
||||
@@ -191,7 +171,6 @@ function GridCardGraph({
|
||||
const queryResponse = useGetQueryRange(
|
||||
{
|
||||
...requestData,
|
||||
variables: getDashboardVariables(variables),
|
||||
selectedTime: widget.timePreferance || 'GLOBAL_TIME',
|
||||
globalSelectedInterval:
|
||||
widget?.panelTypes === PANEL_TYPES.LIST && isLogsQuery
|
||||
@@ -214,14 +193,6 @@ function GridCardGraph({
|
||||
widget.timePreferance,
|
||||
widget.fillSpans,
|
||||
requestData,
|
||||
variables
|
||||
? Object.entries(variables).reduce((acc, [id, variable]) => {
|
||||
if (variable.name && referencedVariableNames.includes(variable.name)) {
|
||||
return { ...acc, [id]: variable.selectedValue };
|
||||
}
|
||||
return acc;
|
||||
}, {})
|
||||
: {},
|
||||
...(customTimeRange && customTimeRange.startTime && customTimeRange.endTime
|
||||
? [customTimeRange.startTime, customTimeRange.endTime]
|
||||
: []),
|
||||
@@ -303,9 +274,7 @@ function GridCardGraph({
|
||||
version={version}
|
||||
threshold={threshold}
|
||||
headerMenuList={menuList}
|
||||
isFetchingResponse={
|
||||
queryResponse.isFetching || isPanelWaitingOnAnyVariable
|
||||
}
|
||||
isFetchingResponse={queryResponse.isFetching}
|
||||
setRequestData={setRequestData}
|
||||
onClickHandler={onClickHandler}
|
||||
onDragSelect={onDragSelect}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import {
|
||||
MetricQueryRangeSuccessResponse,
|
||||
@@ -52,7 +51,6 @@ export interface GridCardGraphProps {
|
||||
headerMenuList?: WidgetGraphComponentProps['headerMenuList'];
|
||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||
isQueryEnabled: boolean;
|
||||
variables?: IDashboardVariables;
|
||||
version?: string;
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
customOnDragSelect?: (start: number, end: number) => void;
|
||||
|
||||
@@ -26,8 +26,8 @@ jest.mock(
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('hooks/dashboard/useDashboardVariablesByType', () => ({
|
||||
useDashboardVariablesByType: (): unknown[] => mockDynamicVariables,
|
||||
jest.mock('hooks/dashboard/useDynamicVariableSuggestions', () => ({
|
||||
useDynamicVariableSuggestions: (): unknown[] => mockDynamicVariables,
|
||||
}));
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
@@ -64,11 +64,12 @@ describe('useResolveQuery', () => {
|
||||
expect(resolved).toBe(QUERY);
|
||||
});
|
||||
|
||||
it('resolves through substitute_vars when the dashboard has variables', async () => {
|
||||
it('resolves through substitute_vars when the dashboard has dynamic variables', async () => {
|
||||
mockGetSubstituteVars.mockResolvedValue({
|
||||
httpStatusCode: 200,
|
||||
data: { compositeQuery: {} },
|
||||
});
|
||||
mockDynamicVariables.push({ name: 'env', attribute: 'deployment.env' });
|
||||
|
||||
const { result } = renderHook(() => useUpdatedQuery(), {
|
||||
wrapper: MockQueryClientProvider,
|
||||
@@ -76,13 +77,6 @@ describe('useResolveQuery', () => {
|
||||
|
||||
const resolved = await result.current.getUpdatedQuery({
|
||||
widgetConfig: WIDGET_CONFIG,
|
||||
dashboardData: {
|
||||
data: {
|
||||
variables: {
|
||||
env: { name: 'env', selectedValue: 'prod' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockGetSubstituteVars).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -7,8 +7,7 @@ import { getSubstituteVars } from 'api/dashboard/substitute_vars';
|
||||
import { prepareQueryRangePayloadV5 } from 'api/v5/v5';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { timePreferenceType } from 'constants/timePreference';
|
||||
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
|
||||
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
|
||||
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@@ -21,7 +20,6 @@ interface UseUpdatedQueryOptions {
|
||||
panelTypes: PANEL_TYPES;
|
||||
timePreferance: timePreferenceType;
|
||||
};
|
||||
dashboardData?: any;
|
||||
}
|
||||
|
||||
interface UseUpdatedQueryResult {
|
||||
@@ -37,34 +35,27 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
|
||||
|
||||
const queryRangeMutation = useMutation(getSubstituteVars);
|
||||
|
||||
const dashboardDynamicVariables = useDashboardVariablesByType(
|
||||
'DYNAMIC',
|
||||
'values',
|
||||
);
|
||||
const dashboardDynamicVariables = useDynamicVariableSuggestions();
|
||||
|
||||
const getUpdatedQuery = useCallback(
|
||||
async ({
|
||||
widgetConfig,
|
||||
dashboardData,
|
||||
}: UseUpdatedQueryOptions): Promise<Query> => {
|
||||
const variables = getDashboardVariables(dashboardData?.data?.variables);
|
||||
|
||||
async ({ widgetConfig }: UseUpdatedQueryOptions): Promise<Query> => {
|
||||
// `/substitute_vars` only rewrites `$variable` references, so on surfaces with no
|
||||
// dashboard behind them (APM, Celery, API monitoring) the round-trip is a no-op.
|
||||
if (isEmpty(variables) && isEmpty(dashboardDynamicVariables)) {
|
||||
if (isEmpty(dashboardDynamicVariables)) {
|
||||
return widgetConfig.query;
|
||||
}
|
||||
|
||||
// Prepare query payload with resolved variables
|
||||
const { queryPayload } = prepareQueryRangePayloadV5({
|
||||
query: widgetConfig.query,
|
||||
graphType: getGraphType(widgetConfig.panelTypes),
|
||||
selectedTime: widgetConfig.timePreferance,
|
||||
globalSelectedInterval,
|
||||
variables,
|
||||
originalGraphType: widgetConfig.panelTypes,
|
||||
dynamicVariables: dashboardDynamicVariables,
|
||||
});
|
||||
const { queryPayload } = prepareQueryRangePayloadV5(
|
||||
{
|
||||
query: widgetConfig.query,
|
||||
graphType: getGraphType(widgetConfig.panelTypes),
|
||||
selectedTime: widgetConfig.timePreferance,
|
||||
globalSelectedInterval,
|
||||
originalGraphType: widgetConfig.panelTypes,
|
||||
},
|
||||
dashboardDynamicVariables,
|
||||
);
|
||||
|
||||
// Execute query and process results
|
||||
const queryResult = await queryRangeMutation.mutateAsync(queryPayload);
|
||||
|
||||
@@ -1,242 +1,40 @@
|
||||
import React from 'react';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
|
||||
import useGetResolvedText from '../useGetResolvedText';
|
||||
|
||||
// Create a mock function that we can modify per test
|
||||
let mockDashboardVariables: IDashboardVariables = {};
|
||||
|
||||
// Mock the useDashboardVariables hook
|
||||
jest.mock('hooks/dashboard/useDashboardVariables', () => ({
|
||||
useDashboardVariables: jest.fn(() => ({
|
||||
dashboardVariables: mockDashboardVariables,
|
||||
})),
|
||||
}));
|
||||
import useGetResolvedText from 'hooks/dashboard/useGetResolvedText';
|
||||
|
||||
describe('useGetResolvedText', () => {
|
||||
const SERVICE_VAR = 'test, app +2-|-test, app, frontend, env';
|
||||
const SEVERITY_VAR = 'DEBUG, INFO-|-DEBUG, INFO';
|
||||
const EXPECTED_FULL_TEXT =
|
||||
'Logs count in test, app, frontend, env in DEBUG, INFO';
|
||||
const TRUNCATED_SERVICE = 'test, app +2';
|
||||
const TEXT_TEMPLATE = 'Logs count in $service.name in $severity';
|
||||
|
||||
const renderHookWithProps = (
|
||||
props: {
|
||||
text: string | React.ReactNode;
|
||||
maxLength?: number;
|
||||
matcher?: string;
|
||||
},
|
||||
variables?: Record<string, string | number | boolean>,
|
||||
): any => {
|
||||
if (variables) {
|
||||
mockDashboardVariables = Object.entries(
|
||||
variables,
|
||||
).reduce<IDashboardVariables>((acc, [key, value]) => {
|
||||
acc[key] = {
|
||||
id: key,
|
||||
name: key,
|
||||
description: '',
|
||||
type: 'CUSTOM' as const,
|
||||
sort: 'DISABLED' as const,
|
||||
multiSelect: false,
|
||||
showALLOption: false,
|
||||
selectedValue: value,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
} else {
|
||||
mockDashboardVariables = {};
|
||||
}
|
||||
return renderHook(() => useGetResolvedText(props));
|
||||
};
|
||||
|
||||
it('should resolve variables with truncated and full text', () => {
|
||||
const text = TEXT_TEMPLATE;
|
||||
const variables = {
|
||||
'service.name': SERVICE_VAR,
|
||||
severity: SEVERITY_VAR,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text }, variables);
|
||||
|
||||
expect(result.current.truncatedText).toBe(
|
||||
`Logs count in ${TRUNCATED_SERVICE} in DEBUG, INFO`,
|
||||
it('returns the text unchanged when it fits within maxLength', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useGetResolvedText({ text: 'Logs count', maxLength: 100 }),
|
||||
);
|
||||
expect(result.current.fullText).toBe(EXPECTED_FULL_TEXT);
|
||||
|
||||
expect(result.current.fullText).toBe('Logs count');
|
||||
expect(result.current.truncatedText).toBe('Logs count');
|
||||
});
|
||||
|
||||
it('should handle text with maxLength truncation', () => {
|
||||
const text = TEXT_TEMPLATE;
|
||||
const variables = {
|
||||
'service.name': SERVICE_VAR,
|
||||
severity: SEVERITY_VAR,
|
||||
};
|
||||
it('returns the text unchanged when no maxLength is given', () => {
|
||||
const text = 'a'.repeat(200);
|
||||
const { result } = renderHook(() => useGetResolvedText({ text }));
|
||||
|
||||
const { result } = renderHookWithProps({ text, maxLength: 20 }, variables);
|
||||
|
||||
expect(result.current.truncatedText).toBe('Logs count in test, a...');
|
||||
expect(result.current.fullText).toBe(EXPECTED_FULL_TEXT);
|
||||
});
|
||||
|
||||
it('should handle multiple occurrences of the same variable', () => {
|
||||
const text = 'Logs count in $service.name and $service.name';
|
||||
const variables = {
|
||||
'service.name': SERVICE_VAR,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text }, variables);
|
||||
|
||||
expect(result.current.truncatedText).toBe(
|
||||
'Logs count in test, app +2 and test, app +2',
|
||||
);
|
||||
expect(result.current.fullText).toBe(
|
||||
'Logs count in test, app, frontend, env and test, app, frontend, env',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle different variable formats', () => {
|
||||
const text =
|
||||
'Logs in $service.name, {{service.name}}, [[service.name]] - $dyn-service.name';
|
||||
const variables = {
|
||||
'service.name': SERVICE_VAR,
|
||||
'$dyn-service.name': 'dyn-1, dyn-2',
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text }, variables);
|
||||
|
||||
expect(result.current.truncatedText).toBe(
|
||||
'Logs in test, app +2, test, app +2, test, app +2 - dyn-1, dyn-2',
|
||||
);
|
||||
expect(result.current.fullText).toBe(
|
||||
'Logs in test, app, frontend, env, test, app, frontend, env, test, app, frontend, env - dyn-1, dyn-2',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle custom matcher', () => {
|
||||
const text = 'Logs count in #service.name in #severity';
|
||||
const variables = {
|
||||
'service.name': SERVICE_VAR,
|
||||
severity: SEVERITY_VAR,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text, matcher: '#' }, variables);
|
||||
|
||||
expect(result.current.truncatedText).toBe(
|
||||
'Logs count in test, app +2 in DEBUG, INFO',
|
||||
);
|
||||
expect(result.current.fullText).toBe(EXPECTED_FULL_TEXT);
|
||||
});
|
||||
|
||||
it('should handle non-string variable values', () => {
|
||||
const text = 'Count: $count, Active: $active';
|
||||
const variables = {
|
||||
count: 42,
|
||||
active: true,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text }, variables);
|
||||
|
||||
expect(result.current.fullText).toBe('Count: 42, Active: true');
|
||||
expect(result.current.truncatedText).toBe('Count: 42, Active: true');
|
||||
});
|
||||
|
||||
it('should keep original text for undefined variables', () => {
|
||||
const text = 'Logs count in $service.name in $unknown';
|
||||
const variables = {
|
||||
'service.name': SERVICE_VAR,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text }, variables);
|
||||
|
||||
expect(result.current.truncatedText).toBe(
|
||||
'Logs count in test, app +2 in $unknown',
|
||||
);
|
||||
expect(result.current.fullText).toBe(
|
||||
'Logs count in test, app, frontend, env in $unknown',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle non-string text input (ReactNode)', () => {
|
||||
const reactNodeText = <div>Test ReactNode</div>;
|
||||
const variables = {
|
||||
'service.name': SERVICE_VAR,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps(
|
||||
{
|
||||
text: reactNodeText,
|
||||
},
|
||||
variables,
|
||||
);
|
||||
|
||||
// Should return the ReactNode unchanged
|
||||
expect(result.current.fullText).toBe(reactNodeText);
|
||||
expect(result.current.truncatedText).toBe(reactNodeText);
|
||||
});
|
||||
|
||||
it('should handle number input', () => {
|
||||
const text = 123;
|
||||
const variables = {
|
||||
'service.name': SERVICE_VAR,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps(
|
||||
{
|
||||
text,
|
||||
},
|
||||
variables,
|
||||
);
|
||||
|
||||
// Should return the number unchanged
|
||||
expect(result.current.fullText).toBe(text);
|
||||
expect(result.current.truncatedText).toBe(text);
|
||||
});
|
||||
|
||||
it('should handle boolean input', () => {
|
||||
const text = true;
|
||||
const variables = {
|
||||
'service.name': SERVICE_VAR,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps(
|
||||
{
|
||||
text,
|
||||
},
|
||||
variables,
|
||||
it('truncates to maxLength with an ellipsis and keeps the full text', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useGetResolvedText({ text: 'Logs count in production', maxLength: 20 }),
|
||||
);
|
||||
|
||||
// Should return the boolean unchanged
|
||||
expect(result.current.fullText).toBe(text);
|
||||
expect(result.current.truncatedText).toBe(text);
|
||||
expect(result.current.truncatedText).toBe('Logs count in pro...');
|
||||
expect(result.current.truncatedText).toHaveLength(20);
|
||||
expect(result.current.fullText).toBe('Logs count in production');
|
||||
});
|
||||
|
||||
it('should handle complex variable names with improved patterns', () => {
|
||||
const text = 'API: $api.v1.endpoint Config: $config.database.host';
|
||||
const variables = {
|
||||
'api.v1.endpoint': '/users',
|
||||
'config.database.host': 'localhost:5432',
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text }, variables);
|
||||
|
||||
expect(result.current.fullText).toBe('API: /users Config: localhost:5432');
|
||||
expect(result.current.truncatedText).toBe(
|
||||
'API: /users Config: localhost:5432',
|
||||
it('passes non-string content through untouched', () => {
|
||||
const node = <span>title</span>;
|
||||
const { result } = renderHook(() =>
|
||||
useGetResolvedText({ text: node, maxLength: 2 }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should stop at punctuation boundaries correctly', () => {
|
||||
const text = 'Status: $service.name, Error: $error.type;';
|
||||
const variables = {
|
||||
'service.name': 'web-api',
|
||||
'error.type': 'timeout',
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text }, variables);
|
||||
|
||||
expect(result.current.fullText).toBe('Status: web-api, Error: timeout;');
|
||||
expect(result.current.truncatedText).toBe('Status: web-api, Error: timeout;');
|
||||
expect(result.current.fullText).toBe(node);
|
||||
expect(result.current.truncatedText).toBe(node);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { dashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||
import { IDashboardVariablesStoreState } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import {
|
||||
VariableFetchState,
|
||||
variableFetchStore,
|
||||
} from 'providers/Dashboard/store/variableFetchStore';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/variables';
|
||||
|
||||
import { useIsPanelWaitingOnVariable } from '../useVariableFetchState';
|
||||
|
||||
function makeVariable(
|
||||
overrides: Partial<IDashboardVariable> & { id: string },
|
||||
): IDashboardVariable {
|
||||
return {
|
||||
name: overrides.id,
|
||||
description: '',
|
||||
type: 'QUERY',
|
||||
sort: 'DISABLED',
|
||||
multiSelect: false,
|
||||
showALLOption: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function resetStores(): void {
|
||||
variableFetchStore.set(() => ({
|
||||
states: {},
|
||||
lastUpdated: {},
|
||||
cycleIds: {},
|
||||
}));
|
||||
dashboardVariablesStore.set(() => ({
|
||||
dashboardId: '',
|
||||
variables: {},
|
||||
sortedVariablesArray: [],
|
||||
dependencyData: null,
|
||||
variableTypes: {},
|
||||
dynamicVariableOrder: [],
|
||||
}));
|
||||
}
|
||||
|
||||
function setFetchStates(states: Record<string, VariableFetchState>): void {
|
||||
variableFetchStore.set(() => ({
|
||||
states,
|
||||
lastUpdated: {},
|
||||
cycleIds: {},
|
||||
}));
|
||||
}
|
||||
|
||||
function setDashboardVariables(
|
||||
overrides: Partial<IDashboardVariablesStoreState>,
|
||||
): void {
|
||||
dashboardVariablesStore.set(() => ({
|
||||
dashboardId: '',
|
||||
variables: {},
|
||||
sortedVariablesArray: [],
|
||||
dependencyData: null,
|
||||
variableTypes: {},
|
||||
dynamicVariableOrder: [],
|
||||
...overrides,
|
||||
}));
|
||||
}
|
||||
|
||||
describe('useIsPanelWaitingOnVariable', () => {
|
||||
beforeEach(() => {
|
||||
resetStores();
|
||||
});
|
||||
|
||||
it('should return false when variableNames is empty', () => {
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable([]));
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when all referenced variables are idle', () => {
|
||||
setFetchStates({ a: 'idle', b: 'idle' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
a: makeVariable({ id: 'a', selectedValue: 'val1' }),
|
||||
b: makeVariable({ id: 'b', selectedValue: 'val2' }),
|
||||
},
|
||||
variableTypes: { a: 'QUERY', b: 'QUERY' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a', 'b']));
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when a variable is loading with empty selectedValue', () => {
|
||||
setFetchStates({ a: 'loading' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
a: makeVariable({ id: 'a', selectedValue: undefined }),
|
||||
},
|
||||
variableTypes: { a: 'QUERY' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when a variable is waiting with empty selectedValue', () => {
|
||||
setFetchStates({ a: 'waiting' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
a: makeVariable({ id: 'a', selectedValue: '' }),
|
||||
},
|
||||
variableTypes: { a: 'QUERY' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when a variable is revalidating with empty selectedValue', () => {
|
||||
setFetchStates({ a: 'revalidating' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
a: makeVariable({ id: 'a', selectedValue: undefined }),
|
||||
},
|
||||
variableTypes: { a: 'QUERY' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when a variable is loading but has a selectedValue', () => {
|
||||
setFetchStates({ a: 'loading' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
a: makeVariable({ id: 'a', selectedValue: 'some-value' }),
|
||||
},
|
||||
variableTypes: { a: 'QUERY' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for DYNAMIC variable with allSelected=true that is loading but has a selectedValue', () => {
|
||||
setFetchStates({ dyn: 'loading' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
dyn: makeVariable({
|
||||
id: 'dyn',
|
||||
type: 'DYNAMIC',
|
||||
selectedValue: 'some-val',
|
||||
allSelected: true,
|
||||
}),
|
||||
},
|
||||
variableTypes: { dyn: 'DYNAMIC' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['dyn']));
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for DYNAMIC variable with allSelected=true that is waiting but has a selectedValue', () => {
|
||||
setFetchStates({ dyn: 'waiting' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
dyn: makeVariable({
|
||||
id: 'dyn',
|
||||
type: 'DYNAMIC',
|
||||
selectedValue: 'val',
|
||||
allSelected: true,
|
||||
}),
|
||||
},
|
||||
variableTypes: { dyn: 'DYNAMIC' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['dyn']));
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for DYNAMIC variable with allSelected=true that is idle', () => {
|
||||
setFetchStates({ dyn: 'idle' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
dyn: makeVariable({
|
||||
id: 'dyn',
|
||||
type: 'DYNAMIC',
|
||||
selectedValue: 'val',
|
||||
allSelected: true,
|
||||
}),
|
||||
},
|
||||
variableTypes: { dyn: 'DYNAMIC' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['dyn']));
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for non-DYNAMIC variable with allSelected=false and non-empty value that is loading', () => {
|
||||
setFetchStates({ a: 'loading' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
a: makeVariable({
|
||||
id: 'a',
|
||||
selectedValue: 'val',
|
||||
allSelected: false,
|
||||
}),
|
||||
},
|
||||
variableTypes: { a: 'QUERY' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if any one of multiple variables is blocking', () => {
|
||||
setFetchStates({ a: 'idle', b: 'loading' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
a: makeVariable({ id: 'a', selectedValue: 'val' }),
|
||||
b: makeVariable({ id: 'b', selectedValue: undefined }),
|
||||
},
|
||||
variableTypes: { a: 'QUERY', b: 'QUERY' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a', 'b']));
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when variable has no entry in fetch store (treated as idle)', () => {
|
||||
setFetchStates({}); // no state entry for 'a'
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
a: makeVariable({ id: 'a', selectedValue: 'val' }),
|
||||
},
|
||||
variableTypes: { a: 'QUERY' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when variable is in error state with empty selectedValue', () => {
|
||||
setFetchStates({ a: 'error' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
a: makeVariable({ id: 'a', selectedValue: undefined }),
|
||||
},
|
||||
variableTypes: { a: 'QUERY' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should react to store updates', () => {
|
||||
setFetchStates({ a: 'loading' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
a: makeVariable({ id: 'a', selectedValue: undefined }),
|
||||
},
|
||||
variableTypes: { a: 'QUERY' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
|
||||
expect(result.current).toBe(true);
|
||||
|
||||
// Simulate variable fetch completing
|
||||
act(() => {
|
||||
variableFetchStore.update((d) => {
|
||||
d.states.a = 'idle';
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle DYNAMIC variable with allSelected=false and empty selectedValue as blocking', () => {
|
||||
setFetchStates({ dyn: 'loading' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
dyn: makeVariable({
|
||||
id: 'dyn',
|
||||
type: 'DYNAMIC',
|
||||
selectedValue: undefined,
|
||||
allSelected: false,
|
||||
}),
|
||||
},
|
||||
variableTypes: { dyn: 'DYNAMIC' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['dyn']));
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle variable with array selectedValue as non-blocking when loading', () => {
|
||||
setFetchStates({ a: 'loading' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
a: makeVariable({ id: 'a', selectedValue: ['val1', 'val2'] }),
|
||||
},
|
||||
variableTypes: { a: 'QUERY' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle variable with empty array selectedValue as blocking when loading', () => {
|
||||
setFetchStates({ a: 'loading' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
a: makeVariable({ id: 'a', selectedValue: [] }),
|
||||
},
|
||||
variableTypes: { a: 'QUERY' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['a']));
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should find variable by name when store key differs from variable name', () => {
|
||||
setFetchStates({ myVar: 'loading' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
'uuid-abc-123': makeVariable({
|
||||
id: 'uuid-abc-123',
|
||||
name: 'myVar',
|
||||
selectedValue: undefined,
|
||||
}),
|
||||
},
|
||||
variableTypes: { myVar: 'QUERY' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['myVar']));
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should respect selectedValue when store key differs from variable name', () => {
|
||||
// When the variable has a value, it should not block even if loading
|
||||
setFetchStates({ myVar: 'loading' });
|
||||
setDashboardVariables({
|
||||
variables: {
|
||||
'uuid-abc-123': makeVariable({
|
||||
id: 'uuid-abc-123',
|
||||
name: 'myVar',
|
||||
selectedValue: 'production',
|
||||
}),
|
||||
},
|
||||
variableTypes: { myVar: 'QUERY' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsPanelWaitingOnVariable(['myVar']));
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
@@ -42,38 +41,10 @@ function useContextVariables({
|
||||
// ! To be noted: This customVariables is not Dashboard Custom Variables
|
||||
customVariables,
|
||||
}: UseContextVariablesProps): UseContextVariablesResult {
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
// Extract dashboard variables
|
||||
const processedDashboardVariables = useMemo(() => {
|
||||
return Object.entries(dashboardVariables)
|
||||
.filter(([, value]) => value.name)
|
||||
.map(([, value]) => {
|
||||
let processedValue: string | number | boolean;
|
||||
let isArray = false;
|
||||
|
||||
if (Array.isArray(value.selectedValue)) {
|
||||
processedValue = value.selectedValue.join(', ');
|
||||
isArray = true;
|
||||
} else if (value.selectedValue != null) {
|
||||
processedValue = value.selectedValue;
|
||||
} else {
|
||||
processedValue = '';
|
||||
}
|
||||
|
||||
return {
|
||||
name: value.name || '',
|
||||
value: processedValue,
|
||||
source: 'dashboard' as const,
|
||||
isArray,
|
||||
originalValue: value.selectedValue,
|
||||
};
|
||||
});
|
||||
}, [dashboardVariables]);
|
||||
|
||||
// Extract global variables
|
||||
const globalVariables = useMemo(
|
||||
() => [
|
||||
@@ -109,12 +80,8 @@ function useContextVariables({
|
||||
|
||||
// Combine all variables
|
||||
const allVariables = useMemo(
|
||||
() => [
|
||||
...processedDashboardVariables,
|
||||
...globalVariables,
|
||||
...customVariablesList,
|
||||
],
|
||||
[processedDashboardVariables, globalVariables, customVariablesList],
|
||||
() => [...globalVariables, ...customVariablesList],
|
||||
[globalVariables, customVariablesList],
|
||||
);
|
||||
|
||||
// Create processed variables with truncation logic
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
||||
import { dashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||
import {
|
||||
IDashboardVariablesStoreState,
|
||||
IUseDashboardVariablesReturn,
|
||||
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
|
||||
/**
|
||||
* Generic selector hook for dashboard variables store
|
||||
* Allows granular subscriptions to any part of the store state
|
||||
*
|
||||
* @example
|
||||
* ! Select top-level field
|
||||
* const variables = useDashboardVariablesSelector(s => s.variables);
|
||||
*
|
||||
* ! Select specific variable
|
||||
* const fooVar = useDashboardVariablesSelector(s => s.variables['foo']);
|
||||
*
|
||||
* ! Select derived value
|
||||
* const hasVariables = useDashboardVariablesSelector(s => Object.keys(s.variables).length > 0);
|
||||
*/
|
||||
export const useDashboardVariablesSelector = <T>(
|
||||
selector: (state: IDashboardVariablesStoreState) => T,
|
||||
): T => {
|
||||
const selectorRef = useRef(selector);
|
||||
selectorRef.current = selector;
|
||||
|
||||
const getSnapshot = useCallback(
|
||||
() => selectorRef.current(dashboardVariablesStore.getSnapshot()),
|
||||
[],
|
||||
);
|
||||
|
||||
return useSyncExternalStore(dashboardVariablesStore.subscribe, getSnapshot);
|
||||
};
|
||||
|
||||
export const useDashboardVariables = (): IUseDashboardVariablesReturn => {
|
||||
const dashboardVariables = useDashboardVariablesSelector((s) => s.variables);
|
||||
|
||||
return { dashboardVariables };
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
IDashboardVariable,
|
||||
TVariableQueryType,
|
||||
} from 'types/api/dashboard/variables';
|
||||
|
||||
import { useDashboardVariables } from './useDashboardVariables';
|
||||
|
||||
export function useDashboardVariablesByType(
|
||||
variableType: TVariableQueryType,
|
||||
returnType: 'values',
|
||||
): IDashboardVariable[];
|
||||
export function useDashboardVariablesByType(
|
||||
variableType: TVariableQueryType,
|
||||
returnType?: 'entries',
|
||||
): [string, IDashboardVariable][];
|
||||
export function useDashboardVariablesByType(
|
||||
variableType: TVariableQueryType,
|
||||
returnType?: 'values' | 'entries',
|
||||
): IDashboardVariable[] | [string, IDashboardVariable][] {
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
|
||||
return useMemo(() => {
|
||||
const entries = Object.entries(dashboardVariables || {}).filter(
|
||||
(entry): entry is [string, IDashboardVariable] =>
|
||||
Boolean(entry[1].name) && entry[1].type === variableType,
|
||||
);
|
||||
return returnType === 'values' ? entries.map(([, value]) => value) : entries;
|
||||
}, [dashboardVariables, variableType, returnType]);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import {
|
||||
DynamicVariableSuggestion,
|
||||
useDynamicVariableSuggestionsStore,
|
||||
} from 'providers/Dashboard/store/dynamicVariableSuggestions';
|
||||
|
||||
/**
|
||||
* Dynamic variables published by the dashboard currently open, so the query
|
||||
* builder can offer `$variable` as a value for the key each one backs. Empty on
|
||||
* surfaces with no dashboard behind them (APM, Celery, messaging queues).
|
||||
*/
|
||||
export function useDynamicVariableSuggestions(): DynamicVariableSuggestion[] {
|
||||
return useDynamicVariableSuggestionsStore((state) => state.suggestions);
|
||||
}
|
||||
@@ -1,18 +1,8 @@
|
||||
// this hook is used to get the resolved text of a variable, lets say we have a text - "Logs count in $service.name in $severity and $service.name and $severity $service.name"
|
||||
// and the values of service.name and severity are "service1" and "error" respectively, then the resolved text should be "Logs count in service1 in error and service1 and error service1"
|
||||
// is case of the multiple variables value, make them comma separated
|
||||
// also have a prop saying max length post that you should truncate the text with "..."
|
||||
// return value should be a full text string, and a truncated text string (if max length is provided)
|
||||
|
||||
import { ReactNode, useCallback, useMemo } from 'react';
|
||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
|
||||
interface UseGetResolvedTextProps {
|
||||
text: string | ReactNode;
|
||||
variables?: Record<string, string | number | boolean>;
|
||||
maxLength?: number;
|
||||
matcher?: string;
|
||||
maxValues?: number; // Maximum number of values to show before adding +n more
|
||||
}
|
||||
|
||||
interface ResolvedTextResult {
|
||||
@@ -20,173 +10,23 @@ interface ResolvedTextResult {
|
||||
truncatedText: string | ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a panel title alongside a copy truncated to `maxLength`, so a card can
|
||||
* show the short form and keep the full string for its tooltip. Non-string content
|
||||
* passes through untouched.
|
||||
*/
|
||||
function useGetResolvedText({
|
||||
text,
|
||||
maxLength,
|
||||
matcher = '$',
|
||||
maxValues = 2, // Default to showing 2 values before +n more
|
||||
}: UseGetResolvedTextProps): ResolvedTextResult {
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
const isString = typeof text === 'string';
|
||||
|
||||
const processedDashboardVariables = useMemo(() => {
|
||||
return Object.entries(dashboardVariables).reduce<
|
||||
Record<string, string | number | boolean>
|
||||
>((acc, [, value]) => {
|
||||
if (!value.name) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Handle array values
|
||||
if (Array.isArray(value.selectedValue)) {
|
||||
acc[value.name] = value.selectedValue.join(', ');
|
||||
} else if (value.selectedValue != null) {
|
||||
acc[value.name] = value.selectedValue;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}, [dashboardVariables]);
|
||||
|
||||
// Process array values to add +n more notation for truncated text
|
||||
const processedVariables = useMemo(() => {
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
Object.entries(processedDashboardVariables).forEach(([key, value]) => {
|
||||
// If the value contains array data (comma-separated string), format it with +n more
|
||||
if (
|
||||
typeof value === 'string' &&
|
||||
!value.includes('-|-') &&
|
||||
value.includes(',')
|
||||
) {
|
||||
const values = value.split(',').map((v) => v.trim());
|
||||
if (values.length > maxValues) {
|
||||
const visibleValues = values.slice(0, maxValues);
|
||||
const remainingCount = values.length - maxValues;
|
||||
result[key] = `${visibleValues.join(
|
||||
', ',
|
||||
)} +${remainingCount}-|-${values.join(', ')}`;
|
||||
} else {
|
||||
result[key] = `${values.join(', ')}-|-${values.join(', ')}`;
|
||||
}
|
||||
} else {
|
||||
// For values already formatted with -|- or non-array values
|
||||
result[key] = String(value);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [processedDashboardVariables, maxValues]);
|
||||
|
||||
const combinedPattern = useMemo(() => {
|
||||
const escapedMatcher = matcher.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const variablePatterns = [
|
||||
`\\{\\{\\s*?\\.([^\\s}]+?)\\s*?\\}\\}`, // {{.var}}
|
||||
`\\{\\{\\s*([^\\s}]+?)\\s*\\}\\}`, // {{var}}
|
||||
`${escapedMatcher}([^\\s.,;)\\]}>]+(?:\\.[^\\s.,;)\\]}>]+)*)`, // $var.name.path - allows dots but stops at punctuation
|
||||
`\\[\\[\\s*([^\\s\\]]+?)\\s*\\]\\]`, // [[var]]
|
||||
];
|
||||
return new RegExp(variablePatterns.join('|'), 'g');
|
||||
}, [matcher]);
|
||||
|
||||
const extractVarName = useCallback(
|
||||
(match: string): string => {
|
||||
// Extract variable name from different formats
|
||||
const varNamePattern = '[a-zA-Z_\\-][a-zA-Z0-9_.\\-]*';
|
||||
if (match.startsWith('{{')) {
|
||||
const dotMatch = match.match(
|
||||
new RegExp(`\\{\\{\\s*\\.(${varNamePattern})\\s*\\}\\}`),
|
||||
);
|
||||
if (dotMatch) {
|
||||
return dotMatch[1].trim();
|
||||
}
|
||||
const normalMatch = match.match(
|
||||
new RegExp(`\\{\\{\\s*(${varNamePattern})\\s*\\}\\}`),
|
||||
);
|
||||
if (normalMatch) {
|
||||
return normalMatch[1].trim();
|
||||
}
|
||||
} else if (match.startsWith('[[')) {
|
||||
const bracketMatch = match.match(
|
||||
new RegExp(`\\[\\[\\s*(${varNamePattern})\\s*\\]\\]`),
|
||||
);
|
||||
if (bracketMatch) {
|
||||
return bracketMatch[1].trim();
|
||||
}
|
||||
} else if (match.startsWith(matcher)) {
|
||||
// For $ variables, we always want to strip the prefix
|
||||
// unless the full match exists in processedVariables
|
||||
const withoutPrefix = match.substring(matcher.length).trim();
|
||||
const fullMatch = match.trim();
|
||||
|
||||
// If the full match (with prefix) exists, use it
|
||||
if (processedVariables[fullMatch] !== undefined) {
|
||||
return fullMatch;
|
||||
}
|
||||
|
||||
// Otherwise return without prefix
|
||||
return withoutPrefix;
|
||||
}
|
||||
return match;
|
||||
},
|
||||
[matcher, processedVariables],
|
||||
);
|
||||
|
||||
const fullText = useMemo(() => {
|
||||
if (!isString) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return (text as string)?.replace(combinedPattern, (match) => {
|
||||
const varName = extractVarName(match);
|
||||
const value = processedVariables[varName];
|
||||
|
||||
if (value != null) {
|
||||
const parts = value.split('-|-');
|
||||
return parts.length > 1 ? parts[1] : value;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
}, [text, processedVariables, combinedPattern, extractVarName, isString]);
|
||||
|
||||
const truncatedText = useMemo(() => {
|
||||
if (!isString) {
|
||||
if (typeof text !== 'string' || !maxLength || text.length <= maxLength) {
|
||||
return text;
|
||||
}
|
||||
return `${text.substring(0, maxLength - 3)}...`;
|
||||
}, [text, maxLength]);
|
||||
|
||||
const result = (text as string)?.replace(combinedPattern, (match) => {
|
||||
const varName = extractVarName(match);
|
||||
const value = processedVariables[varName];
|
||||
|
||||
if (value != null) {
|
||||
const parts = value.split('-|-');
|
||||
return parts[0] || value;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
|
||||
if (maxLength && result.length > maxLength) {
|
||||
// For the specific test case
|
||||
if (maxLength === 20 && result.startsWith('Logs count in')) {
|
||||
return 'Logs count in test, a...';
|
||||
}
|
||||
|
||||
// General case
|
||||
return `${result.substring(0, maxLength - 3)}...`;
|
||||
}
|
||||
return result;
|
||||
}, [
|
||||
text,
|
||||
processedVariables,
|
||||
combinedPattern,
|
||||
maxLength,
|
||||
extractVarName,
|
||||
isString,
|
||||
]);
|
||||
|
||||
return {
|
||||
fullText,
|
||||
truncatedText,
|
||||
};
|
||||
return { fullText: text, truncatedText };
|
||||
}
|
||||
|
||||
export default useGetResolvedText;
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
import { useCallback, useMemo, useRef, useSyncExternalStore } from 'react';
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
import {
|
||||
IVariableFetchStoreState,
|
||||
VariableFetchState,
|
||||
variableFetchStore,
|
||||
} from 'providers/Dashboard/store/variableFetchStore';
|
||||
|
||||
import { useDashboardVariablesSelector } from './useDashboardVariables';
|
||||
|
||||
/**
|
||||
* Generic selector hook for the variable fetch store.
|
||||
* Same pattern as useDashboardVariablesSelector.
|
||||
*/
|
||||
const useVariableFetchSelector = <T>(
|
||||
selector: (state: IVariableFetchStoreState) => T,
|
||||
): T => {
|
||||
const selectorRef = useRef(selector);
|
||||
selectorRef.current = selector;
|
||||
|
||||
const getSnapshot = useCallback(
|
||||
() => selectorRef.current(variableFetchStore.getSnapshot()),
|
||||
[],
|
||||
);
|
||||
|
||||
return useSyncExternalStore(variableFetchStore.subscribe, getSnapshot);
|
||||
};
|
||||
|
||||
interface UseVariableFetchStateReturn {
|
||||
/** The current fetch state for this variable */
|
||||
variableFetchState: VariableFetchState;
|
||||
/** Current fetch cycle — include in react-query keys to auto-cancel stale requests */
|
||||
variableFetchCycleId: number;
|
||||
/** True if this variable is idle (not waiting and not fetching) */
|
||||
isVariableSettled: boolean;
|
||||
/** True if this variable is actively fetching (loading or revalidating) */
|
||||
isVariableFetching: boolean;
|
||||
/** True if this variable has completed at least one fetch cycle */
|
||||
hasVariableFetchedOnce: boolean;
|
||||
/** True if any parent variable hasn't settled yet */
|
||||
isVariableWaitingForDependencies: boolean;
|
||||
/** Message describing what this variable is waiting on, or null if not waiting */
|
||||
variableDependencyWaitMessage?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-variable hook that exposes the fetch state of a single variable.
|
||||
* Reusable by both variable input components and panel components.
|
||||
*
|
||||
* Subscribes to both variableFetchStore (for states) and
|
||||
* dashboardVariablesStore (for parent graph) to compute derived values.
|
||||
*/
|
||||
export function useVariableFetchState(
|
||||
variableName: string,
|
||||
): UseVariableFetchStateReturn {
|
||||
// This variable's fetch state (loading, waiting, idle, etc.)
|
||||
const variableFetchState = useVariableFetchSelector(
|
||||
(s) => s.states[variableName] || 'idle',
|
||||
) as VariableFetchState;
|
||||
|
||||
// All variable states — needed to check if parent variables are still in-flight
|
||||
const allStates = useVariableFetchSelector((s) => s.states);
|
||||
|
||||
// Parent dependency graph — maps each variable to its direct parents
|
||||
// e.g. { "childVariable": ["parentVariable"] } means "childVariable" depends on "parentVariable"
|
||||
const parentGraph = useDashboardVariablesSelector(
|
||||
(s) => s.dependencyData?.parentDependencyGraph,
|
||||
);
|
||||
|
||||
// Timestamp of last successful fetch — 0 means never fetched
|
||||
const lastUpdated = useVariableFetchSelector(
|
||||
(s) => s.lastUpdated[variableName] || 0,
|
||||
);
|
||||
|
||||
// Per-variable cycle counter — used as part of react-query keys
|
||||
// so changing it auto-cancels stale requests for this variable only
|
||||
const variableFetchCycleId = useVariableFetchSelector(
|
||||
(s) => s.cycleIds[variableName] || 0,
|
||||
);
|
||||
|
||||
const isVariableSettled = variableFetchState === 'idle';
|
||||
|
||||
const isVariableFetching =
|
||||
variableFetchState === 'loading' || variableFetchState === 'revalidating';
|
||||
// True after at least one successful fetch — used to show stale data while revalidating
|
||||
const hasVariableFetchedOnce = lastUpdated > 0;
|
||||
|
||||
// Variable type — needed to differentiate waiting messages
|
||||
const variableType = useDashboardVariablesSelector(
|
||||
(s) => s.variableTypes[variableName],
|
||||
);
|
||||
|
||||
// Parent variable names that haven't settled yet
|
||||
const unsettledParents = useMemo(() => {
|
||||
const parents = parentGraph?.[variableName] || [];
|
||||
return parents.filter((p) => (allStates[p] || 'idle') !== 'idle');
|
||||
}, [parentGraph, variableName, allStates]);
|
||||
|
||||
const isVariableWaitingForDependencies = unsettledParents.length > 0;
|
||||
|
||||
const variableDependencyWaitMessage = useMemo(() => {
|
||||
if (variableFetchState !== 'waiting') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (variableType === 'DYNAMIC') {
|
||||
return 'Waiting for all query variable options to load.';
|
||||
}
|
||||
|
||||
if (unsettledParents.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const quoted = unsettledParents.map((p) => `"${p}"`);
|
||||
const names =
|
||||
quoted.length > 1
|
||||
? `${quoted.slice(0, -1).join(', ')} and ${quoted[quoted.length - 1]}`
|
||||
: quoted[0];
|
||||
return `Waiting for options of ${names} to load.`;
|
||||
}, [variableFetchState, variableType, unsettledParents]);
|
||||
|
||||
return {
|
||||
variableFetchState,
|
||||
isVariableSettled,
|
||||
isVariableWaitingForDependencies,
|
||||
variableDependencyWaitMessage,
|
||||
isVariableFetching,
|
||||
hasVariableFetchedOnce,
|
||||
variableFetchCycleId,
|
||||
};
|
||||
}
|
||||
|
||||
export function useIsPanelWaitingOnVariable(variableNames: string[]): boolean {
|
||||
const states = useVariableFetchSelector((s) => s.states);
|
||||
const dashboardVariables = useDashboardVariablesSelector((s) => s.variables);
|
||||
|
||||
return variableNames.some((name) => {
|
||||
const variableFetchState = states[name];
|
||||
const variableData = Object.values(dashboardVariables).find(
|
||||
(v) => v.name === name,
|
||||
);
|
||||
const { selectedValue } = variableData || {};
|
||||
|
||||
const isVariableInFetchingOrWaitingState =
|
||||
variableFetchState === 'loading' ||
|
||||
variableFetchState === 'revalidating' ||
|
||||
variableFetchState === 'waiting';
|
||||
|
||||
return isEmpty(selectedValue) ? isVariableInFetchingOrWaitingState : false;
|
||||
});
|
||||
}
|
||||
@@ -32,12 +32,8 @@ jest.mock(
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('hooks/dashboard/useDashboardVariables', () => ({
|
||||
useDashboardVariables: (): unknown => ({ dashboardVariables: {} }),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/dashboard/useDashboardVariablesByType', () => ({
|
||||
useDashboardVariablesByType: (): unknown => ({}),
|
||||
jest.mock('hooks/dashboard/useDynamicVariableSuggestions', () => ({
|
||||
useDynamicVariableSuggestions: (): unknown[] => [],
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
@@ -46,10 +42,6 @@ jest.mock('hooks/useNotifications', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('lib/dashboardVariables/getDashboardVariables', () => ({
|
||||
getDashboardVariables: (): unknown => ({}),
|
||||
}));
|
||||
|
||||
jest.mock('utils/getGraphType', () => ({
|
||||
getGraphType: jest.fn().mockReturnValue('time_series'),
|
||||
}));
|
||||
|
||||
@@ -11,10 +11,8 @@ import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { MenuItemKeys } from 'container/WidgetCard/Header/contants';
|
||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
|
||||
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -38,11 +36,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
const dashboardDynamicVariables = useDashboardVariablesByType(
|
||||
'DYNAMIC',
|
||||
'values',
|
||||
);
|
||||
const dashboardDynamicVariables = useDynamicVariableSuggestions();
|
||||
|
||||
return useCallback(() => {
|
||||
if (!widget) {
|
||||
@@ -63,15 +57,16 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
queryType: widget.query.queryType,
|
||||
});
|
||||
}
|
||||
const { queryPayload } = prepareQueryRangePayloadV5({
|
||||
query: widget.query,
|
||||
globalSelectedInterval,
|
||||
graphType: getGraphType(widget.panelTypes),
|
||||
selectedTime: widget.timePreferance,
|
||||
variables: getDashboardVariables(dashboardVariables),
|
||||
originalGraphType: widget.panelTypes,
|
||||
dynamicVariables: dashboardDynamicVariables,
|
||||
});
|
||||
const { queryPayload } = prepareQueryRangePayloadV5(
|
||||
{
|
||||
query: widget.query,
|
||||
globalSelectedInterval,
|
||||
graphType: getGraphType(widget.panelTypes),
|
||||
selectedTime: widget.timePreferance,
|
||||
originalGraphType: widget.panelTypes,
|
||||
},
|
||||
dashboardDynamicVariables,
|
||||
);
|
||||
queryRangeMutation.mutate(queryPayload, {
|
||||
onSuccess: (data) => {
|
||||
const updatedQuery = mapQueryDataFromApi(data.data.compositeQuery);
|
||||
@@ -107,7 +102,6 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
globalSelectedInterval,
|
||||
notifications,
|
||||
queryRangeMutation,
|
||||
dashboardVariables,
|
||||
dashboardDynamicVariables,
|
||||
widget,
|
||||
]);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user