mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-07 20:10:42 +01:00
Compare commits
10 Commits
proto/stor
...
chore/remo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2a6e1b786 | ||
|
|
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
|
||||
|
||||
@@ -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:
|
||||
@@ -9217,6 +9627,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 +14961,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 +19719,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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -10522,6 +11034,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 +12399,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 +13162,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));
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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}`)];
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { MAX_QUERY_RETRIES } from 'constants/reactQuery';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { updateBarStepInterval } from 'container/WidgetCard/utils';
|
||||
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
|
||||
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
|
||||
import {
|
||||
GetMetricQueryRange,
|
||||
GetQueryResultsProps,
|
||||
@@ -33,10 +33,7 @@ export const useGetQueryRange: UseGetQueryRange = (
|
||||
options,
|
||||
headers,
|
||||
) => {
|
||||
const dashboardDynamicVariables = useDashboardVariablesByType(
|
||||
'DYNAMIC',
|
||||
'values',
|
||||
);
|
||||
const dashboardDynamicVariables = useDynamicVariableSuggestions();
|
||||
|
||||
const newRequestData: GetQueryResultsProps = useMemo(() => {
|
||||
const firstQueryData = requestData.query.builder?.queryData[0];
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { DynamicVariableSuggestion } from 'providers/Dashboard/store/dynamicVariableSuggestions';
|
||||
import { SuccessResponseV2, Warning } from 'types/api';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/variables';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
@@ -179,7 +179,7 @@ export const getLegend = (
|
||||
export async function GetMetricQueryRange(
|
||||
props: GetQueryResultsProps,
|
||||
version: string,
|
||||
dynamicVariables?: IDashboardVariable[],
|
||||
dynamicVariables: DynamicVariableSuggestion[] = [],
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<MetricQueryRangeSuccessResponse> {
|
||||
@@ -226,10 +226,7 @@ export async function GetMetricQueryRange(
|
||||
}
|
||||
|
||||
if (version === ENTITY_VERSION_V5) {
|
||||
const v5Result = prepareQueryRangePayloadV5({
|
||||
...props,
|
||||
dynamicVariables,
|
||||
});
|
||||
const v5Result = prepareQueryRangePayloadV5(props, dynamicVariables);
|
||||
legendMap = v5Result.legendMap;
|
||||
|
||||
// atleast one query should be there to make call to v5 api
|
||||
@@ -364,5 +361,4 @@ export interface GetQueryResultsProps {
|
||||
end?: number;
|
||||
step?: number;
|
||||
originalGraphType?: PANEL_TYPES;
|
||||
dynamicVariables?: IDashboardVariable[];
|
||||
}
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
import { textContainsVariableReference } from 'lib/dashboardVariables/variableReference';
|
||||
import { IDependencyData } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/variables';
|
||||
|
||||
/**
|
||||
* Inter-variable dependency graph over the shared dashboard-variables store. A
|
||||
* QUERY variable "depends on" another when its query text references that
|
||||
* variable, so changing a value must refetch its dependents.
|
||||
*
|
||||
* Keyed on `IDashboardVariable`. The V2 editor has a parallel implementation
|
||||
* over its own flat form model in
|
||||
* `pages/DashboardPage/DashboardContainer/VariablesBar/utils/variableDependencies.ts`.
|
||||
*/
|
||||
|
||||
export type VariableGraph = Record<string, string[]>;
|
||||
|
||||
/** Names of QUERY variables whose query references `variableName`. */
|
||||
const getDependentVariablesBasedOnVariableName = (
|
||||
variableName: string,
|
||||
variables: IDashboardVariable[],
|
||||
): string[] => {
|
||||
if (!variables || !Array.isArray(variables)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return variables
|
||||
.map((variable) => {
|
||||
if (variable.type === 'QUERY') {
|
||||
const queryValue = variable.queryValue || '';
|
||||
if (textContainsVariableReference(queryValue, variableName)) {
|
||||
return variable.name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((val): val is string => val !== null);
|
||||
};
|
||||
|
||||
/** variable name → its direct dependents (children). */
|
||||
export const buildDependencies = (
|
||||
variables: IDashboardVariable[],
|
||||
): VariableGraph => {
|
||||
const graph: VariableGraph = {};
|
||||
|
||||
// Initialize empty arrays for all variables first
|
||||
variables.forEach((variable) => {
|
||||
if (variable.name) {
|
||||
graph[variable.name] = [];
|
||||
}
|
||||
});
|
||||
|
||||
// For each QUERY variable, add it as a dependent to its referenced variables
|
||||
variables.forEach((variable) => {
|
||||
if (variable.name) {
|
||||
const dependentVariables = getDependentVariablesBasedOnVariableName(
|
||||
variable.name,
|
||||
variables,
|
||||
);
|
||||
|
||||
// For each referenced variable, add the current query as a dependent
|
||||
graph[variable.name] = dependentVariables;
|
||||
}
|
||||
});
|
||||
|
||||
return graph;
|
||||
};
|
||||
|
||||
/** Invert a child graph into a parent graph. */
|
||||
export const buildParentDependencyGraph = (
|
||||
graph: VariableGraph,
|
||||
): VariableGraph => {
|
||||
const parentGraph: VariableGraph = {};
|
||||
|
||||
// Initialize empty arrays for all nodes
|
||||
Object.keys(graph).forEach((node) => {
|
||||
parentGraph[node] = [];
|
||||
});
|
||||
|
||||
// For each node and its children in the original graph
|
||||
Object.entries(graph).forEach(([node, children]) => {
|
||||
// For each child, add the current node as its parent
|
||||
children.forEach((child) => {
|
||||
if (!parentGraph[child]) {
|
||||
parentGraph[child] = [];
|
||||
}
|
||||
parentGraph[child].push(node);
|
||||
});
|
||||
});
|
||||
|
||||
return parentGraph;
|
||||
};
|
||||
|
||||
const collectCyclePath = (
|
||||
graph: VariableGraph,
|
||||
start: string,
|
||||
end: string,
|
||||
): string[] => {
|
||||
const path: string[] = [];
|
||||
let current = start;
|
||||
|
||||
const findParent = (node: string): string | undefined =>
|
||||
Object.keys(graph).find((key) => graph[key]?.includes(node));
|
||||
|
||||
while (current !== end) {
|
||||
const parent = findParent(current);
|
||||
if (!parent) {
|
||||
break;
|
||||
}
|
||||
path.push(parent);
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return [start, ...path];
|
||||
};
|
||||
|
||||
const detectCycle = (
|
||||
graph: VariableGraph,
|
||||
node: string,
|
||||
visited: Set<string>,
|
||||
recStack: Set<string>,
|
||||
): string[] | null => {
|
||||
if (!visited.has(node)) {
|
||||
visited.add(node);
|
||||
recStack.add(node);
|
||||
|
||||
const neighbors = graph[node] || [];
|
||||
let cycleNodes: string[] | null = null;
|
||||
|
||||
neighbors.some((neighbor) => {
|
||||
if (!visited.has(neighbor)) {
|
||||
const foundCycle = detectCycle(graph, neighbor, visited, recStack);
|
||||
if (foundCycle) {
|
||||
cycleNodes = foundCycle;
|
||||
return true;
|
||||
}
|
||||
} else if (recStack.has(neighbor)) {
|
||||
// Found a cycle, collect the cycle nodes
|
||||
cycleNodes = collectCyclePath(graph, node, neighbor);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (cycleNodes) {
|
||||
return cycleNodes;
|
||||
}
|
||||
}
|
||||
recStack.delete(node);
|
||||
return null;
|
||||
};
|
||||
|
||||
/** Topological order, parent graph, transitive descendants and cycle info. */
|
||||
export const buildDependencyGraph = (
|
||||
dependencies: VariableGraph,
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
): IDependencyData => {
|
||||
const inDegree: Record<string, number> = {};
|
||||
const adjList: VariableGraph = {};
|
||||
|
||||
// Initialize in-degree and adjacency list
|
||||
Object.keys(dependencies).forEach((node) => {
|
||||
if (!inDegree[node]) {
|
||||
inDegree[node] = 0;
|
||||
}
|
||||
if (!adjList[node]) {
|
||||
adjList[node] = [];
|
||||
}
|
||||
dependencies[node]?.forEach((child) => {
|
||||
if (!inDegree[child]) {
|
||||
inDegree[child] = 0;
|
||||
}
|
||||
inDegree[child]++;
|
||||
adjList[node].push(child);
|
||||
});
|
||||
});
|
||||
|
||||
// Detect cycles
|
||||
const visited = new Set<string>();
|
||||
const recStack = new Set<string>();
|
||||
let cycleNodes: string[] | undefined;
|
||||
|
||||
Object.keys(dependencies).some((node) => {
|
||||
if (!visited.has(node)) {
|
||||
const foundCycle = detectCycle(dependencies, node, visited, recStack);
|
||||
if (foundCycle) {
|
||||
cycleNodes = foundCycle;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Topological sort using Kahn's Algorithm
|
||||
const queue: string[] = Object.keys(inDegree).filter(
|
||||
(node) => inDegree[node] === 0,
|
||||
);
|
||||
const topologicalOrder: string[] = [];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
if (current === undefined) {
|
||||
break;
|
||||
}
|
||||
topologicalOrder.push(current);
|
||||
|
||||
adjList[current]?.forEach((neighbor) => {
|
||||
inDegree[neighbor]--;
|
||||
if (inDegree[neighbor] === 0) {
|
||||
queue.push(neighbor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const hasCycle = topologicalOrder.length !== Object.keys(dependencies)?.length;
|
||||
|
||||
// Pre-compute transitive descendants by walking topological order in reverse.
|
||||
// Each node's transitive descendants = direct children + their transitive descendants.
|
||||
const transitiveDescendants: VariableGraph = {};
|
||||
for (let i = topologicalOrder.length - 1; i >= 0; i--) {
|
||||
const node = topologicalOrder[i];
|
||||
const desc = new Set<string>();
|
||||
for (const child of adjList[node] || []) {
|
||||
desc.add(child);
|
||||
for (const d of transitiveDescendants[child] || []) {
|
||||
desc.add(d);
|
||||
}
|
||||
}
|
||||
transitiveDescendants[node] = Array.from(desc);
|
||||
}
|
||||
|
||||
return {
|
||||
order: topologicalOrder,
|
||||
graph: adjList,
|
||||
parentDependencyGraph: buildParentDependencyGraph(adjList),
|
||||
transitiveDescendants,
|
||||
hasCycle,
|
||||
cycleNodes,
|
||||
};
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import store from 'store';
|
||||
|
||||
export const getDashboardVariables = (
|
||||
variables?: IDashboardVariables,
|
||||
): Record<string, unknown> => {
|
||||
if (!variables) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const { globalTime } = store.getState();
|
||||
const { start, end } = getStartEndRangeTime({
|
||||
type: 'GLOBAL_TIME',
|
||||
interval: globalTime.selectedTime,
|
||||
});
|
||||
|
||||
const variablesTuple: Record<string, unknown> = {
|
||||
SIGNOZ_START_TIME: parseInt(start, 10) * 1e3,
|
||||
SIGNOZ_END_TIME: parseInt(end, 10) * 1e3,
|
||||
};
|
||||
|
||||
Object.entries(variables).forEach(([, value]) => {
|
||||
if (value?.name) {
|
||||
variablesTuple[value.name] =
|
||||
value?.type === 'DYNAMIC' &&
|
||||
value?.allSelected &&
|
||||
value?.showALLOption &&
|
||||
value?.multiSelect
|
||||
? '__all__'
|
||||
: value?.selectedValue;
|
||||
}
|
||||
});
|
||||
|
||||
return variablesTuple;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return {};
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getToolTipValue } from 'components/Graph/yAxisConfig';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import uPlot, { Axis } from 'uplot';
|
||||
|
||||
import { uPlotXAxisValuesFormat } from '../../uPlotLib/utils/constants';
|
||||
@@ -7,11 +6,6 @@ import getGridColor from '../../uPlotLib/utils/getGridColor';
|
||||
import { buildYAxisSizeCalculator } from '../utils/axis';
|
||||
import { AxisProps, ConfigBuilder } from './types';
|
||||
|
||||
const PANEL_TYPES_WITH_X_AXIS_DATETIME_FORMAT = [
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
PANEL_TYPES.BAR,
|
||||
];
|
||||
|
||||
/**
|
||||
* Builder for uPlot axis configuration
|
||||
* Handles creation and merging of axis settings
|
||||
@@ -67,12 +61,9 @@ export class UPlotAxisBuilder extends ConfigBuilder<AxisProps, Axis> {
|
||||
* Build values formatter for X-axis (time)
|
||||
*/
|
||||
private buildXAxisValuesFormatter(): uPlot.Axis.Values | undefined {
|
||||
const { panelType } = this.props;
|
||||
const { isTimeAxis } = this.props;
|
||||
|
||||
if (
|
||||
panelType &&
|
||||
PANEL_TYPES_WITH_X_AXIS_DATETIME_FORMAT.includes(panelType)
|
||||
) {
|
||||
if (isTimeAxis) {
|
||||
return uPlotXAxisValuesFormat as uPlot.Axis.Values;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getToolTipValue } from 'components/Graph/yAxisConfig';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { uPlotXAxisValuesFormat } from 'lib/uPlotLib/utils/constants';
|
||||
import type uPlot from 'uplot';
|
||||
|
||||
@@ -137,11 +136,11 @@ describe('UPlotAxisBuilder', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('uses time-based X-axis values formatter for time-series like panels', () => {
|
||||
it('uses time-based X-axis values formatter when the caller declares a time axis', () => {
|
||||
const builder = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
scaleKey: 'x',
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
isTimeAxis: true,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -150,11 +149,11 @@ describe('UPlotAxisBuilder', () => {
|
||||
expect(config.values).toBe(uPlotXAxisValuesFormat);
|
||||
});
|
||||
|
||||
it('does not attach X-axis datetime formatter when panel type is not supported', () => {
|
||||
it('does not attach X-axis datetime formatter for a non-time axis', () => {
|
||||
const builder = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
scaleKey: 'x',
|
||||
panelType: PANEL_TYPES.LIST, // not in PANEL_TYPES_WITH_X_AXIS_DATETIME_FORMAT
|
||||
isTimeAxis: false,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -290,22 +289,9 @@ describe('UPlotAxisBuilder', () => {
|
||||
expect(config.space).toBe(50);
|
||||
});
|
||||
|
||||
it('includes PANEL_TYPES.BAR and PANEL_TYPES.TIME_SERIES in X-axis datetime formatter', () => {
|
||||
const barBuilder = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
scaleKey: 'x',
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
}),
|
||||
);
|
||||
expect(barBuilder.getConfig().values).toBe(uPlotXAxisValuesFormat);
|
||||
|
||||
const timeSeriesBuilder = new UPlotAxisBuilder(
|
||||
createAxisProps({
|
||||
scaleKey: 'x',
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
}),
|
||||
);
|
||||
expect(timeSeriesBuilder.getConfig().values).toBe(uPlotXAxisValuesFormat);
|
||||
it('omits the X-axis datetime formatter when no time axis is declared', () => {
|
||||
const builder = new UPlotAxisBuilder(createAxisProps({ scaleKey: 'x' }));
|
||||
expect(builder.getConfig().values).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the existing size when cycleNum > 1', () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PrecisionOption } from 'components/Graph/types';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import uPlot, { Series } from 'uplot';
|
||||
|
||||
import { ThresholdsDrawHookOptions } from '../hooks/types';
|
||||
@@ -53,31 +52,51 @@ export interface ConfigBuilderProps {
|
||||
* Props for configuring an axis
|
||||
*/
|
||||
export interface AxisProps {
|
||||
/** Scale this axis is drawn against — `'x'` / `'y'`, matching an `addScale` key. Also
|
||||
* selects the default tick formatter and sizing (x: time, y: value + unit). */
|
||||
scaleKey: string;
|
||||
/** Axis title drawn alongside the ticks; omitted when there's nothing to name. */
|
||||
label?: string;
|
||||
/** Render the axis at all; false keeps the scale but draws no ticks or labels. */
|
||||
show?: boolean;
|
||||
side?: 0 | 1 | 2 | 3; // top, right, bottom, left
|
||||
/** Which edge of the plot the axis sits on: 0 | 1 | 2 | 3 — top, right, bottom, left. */
|
||||
side?: 0 | 1 | 2 | 3;
|
||||
/** Tick/label color. Defaults to black or white from `isDarkMode`. */
|
||||
stroke?: string;
|
||||
/** Partial override of the grid lines; unset keys fall back to the theme defaults. */
|
||||
grid?: {
|
||||
stroke?: string;
|
||||
width?: number;
|
||||
show?: boolean;
|
||||
};
|
||||
/** Partial override of the tick marks; provided as-is to uPlot when set. */
|
||||
ticks?: {
|
||||
stroke?: string;
|
||||
width?: number;
|
||||
show?: boolean;
|
||||
size?: number;
|
||||
};
|
||||
/** Explicit tick formatter, replacing the scale's default (time / unit-formatted). */
|
||||
values?: uPlot.Axis.Values;
|
||||
/** Pixels between the ticks and their labels; also feeds the y axis width calculation. */
|
||||
gap?: number;
|
||||
/** Explicit axis thickness. Left unset, the y axis sizes itself to its widest label. */
|
||||
size?: uPlot.Axis.Size;
|
||||
formatValue?: (v: number) => string;
|
||||
space?: number; // Space for log scale axes
|
||||
/** Minimum pixels between ticks, capping how many uPlot draws. For log scale axes. */
|
||||
space?: number;
|
||||
/** Picks the dark or light default for stroke and grid color. */
|
||||
isDarkMode?: boolean;
|
||||
/** Axis is on a log scale — thins the grid lines to keep dense decades readable. */
|
||||
isLogScale?: boolean;
|
||||
/** Unit the y axis ticks are formatted in (`spec.formatting.unit`). */
|
||||
yAxisUnit?: string;
|
||||
panelType?: PANEL_TYPES;
|
||||
/**
|
||||
* X axis carries timestamps, so its ticks format as dates/times. Declared by the caller
|
||||
* rather than inferred from a panel type — a chart whose x axis is buckets or categories
|
||||
* (histogram) leaves it off.
|
||||
*/
|
||||
isTimeAxis?: boolean;
|
||||
/** Decimal places for y axis tick values; unset lets the unit formatter decide. */
|
||||
decimalPrecision?: PrecisionOption;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { PanelMode } from 'lib/visualization/panels/types';
|
||||
import { plotsTimeOnXAxis } from 'lib/visualization/panels/utils/panelAxis';
|
||||
|
||||
export interface BaseConfigBuilderProps {
|
||||
id: string;
|
||||
@@ -124,7 +125,7 @@ export function buildBaseConfig({
|
||||
side: 2,
|
||||
isDarkMode,
|
||||
isLogScale,
|
||||
panelType,
|
||||
isTimeAxis: plotsTimeOnXAxis(panelType),
|
||||
});
|
||||
|
||||
builder.addAxis({
|
||||
@@ -134,7 +135,6 @@ export function buildBaseConfig({
|
||||
isDarkMode,
|
||||
isLogScale,
|
||||
yAxisUnit,
|
||||
panelType,
|
||||
});
|
||||
|
||||
return builder;
|
||||
|
||||
9
frontend/src/lib/visualization/panels/utils/panelAxis.ts
Normal file
9
frontend/src/lib/visualization/panels/utils/panelAxis.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
|
||||
/**
|
||||
* Whether the panel type plots time on X. Graph and bar do; the rest drawn through
|
||||
* `buildBaseConfig` — histogram buckets, billing categories — plot a value there instead.
|
||||
*/
|
||||
export function plotsTimeOnXAxis(panelType: PANEL_TYPES): boolean {
|
||||
return panelType === PANEL_TYPES.TIME_SERIES || panelType === PANEL_TYPES.BAR;
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.sche
|
||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ClickHouseQueryContainer from 'container/QueryBuilder/rawQueryEditors/ClickHouse';
|
||||
import PromQLQueryContainer from 'container/QueryBuilder/rawQueryEditors/PromQL';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
@@ -64,8 +63,12 @@ function PanelEditorQueryBuilder({
|
||||
footer,
|
||||
stickyHeader = true,
|
||||
}: PanelEditorQueryBuilderProps): JSX.Element {
|
||||
// The shared QueryBuilderV2 / list-view checks still speak the legacy PANEL_TYPES.
|
||||
// The shared QueryBuilderV2 provider still speaks the legacy PANEL_TYPES; what the
|
||||
// builder offers for this kind comes from the kind's own declaration.
|
||||
const panelType = PANEL_KIND_TO_PANEL_TYPE[panelKind];
|
||||
// Raw rows: the builder drops its aggregation controls, and with them the trace
|
||||
// operator that combines aggregated trace queries (V1 parity).
|
||||
const isListViewPanel = panelKind === 'signoz/ListPanel';
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
@@ -112,9 +115,9 @@ function PanelEditorQueryBuilder({
|
||||
<QueryBuilderV2
|
||||
panelType={panelType}
|
||||
filterConfigs={filterConfigs}
|
||||
showTraceOperator={panelType !== PANEL_TYPES.LIST}
|
||||
showTraceOperator={!isListViewPanel}
|
||||
version="v3"
|
||||
isListViewPanel={panelType === PANEL_TYPES.LIST}
|
||||
isListViewPanel={isListViewPanel}
|
||||
queryComponents={{}}
|
||||
signalSourceChangeEnabled
|
||||
savePreviousQuery
|
||||
@@ -148,7 +151,7 @@ function PanelEditorQueryBuilder({
|
||||
),
|
||||
children: queryTypeComponents[queryType].component,
|
||||
}));
|
||||
}, [panelKind, panelType, filterConfigs, isDarkMode]);
|
||||
}, [panelKind, panelType, filterConfigs, isDarkMode, isListViewPanel]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -60,6 +60,7 @@ function renderBuilder(
|
||||
function lastQueryBuilderProps(): {
|
||||
panelType: string;
|
||||
isListViewPanel: boolean;
|
||||
showTraceOperator: boolean;
|
||||
filterConfigs: unknown;
|
||||
} {
|
||||
const calls = mockQueryBuilderV2.mock.calls;
|
||||
@@ -115,6 +116,9 @@ describe('PanelEditorQueryBuilder field visibility (driven by the capabilities g
|
||||
const props = lastQueryBuilderProps();
|
||||
expect(props.panelType).toBe('graph');
|
||||
expect(props.isListViewPanel).toBe(false);
|
||||
// The trace operator combines aggregated trace queries, so it rides along with
|
||||
// the aggregation controls.
|
||||
expect(props.showTraceOperator).toBe(true);
|
||||
expect(props.filterConfigs).toStrictEqual({});
|
||||
});
|
||||
|
||||
@@ -124,6 +128,7 @@ describe('PanelEditorQueryBuilder field visibility (driven by the capabilities g
|
||||
const props = lastQueryBuilderProps();
|
||||
expect(props.panelType).toBe('list');
|
||||
expect(props.isListViewPanel).toBe(true);
|
||||
expect(props.showTraceOperator).toBe(false);
|
||||
expect(props.filterConfigs).toStrictEqual({
|
||||
stepInterval: { isHidden: true, isDisabled: true },
|
||||
having: { isHidden: true, isDisabled: true },
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
import { Spline } from '@signozhq/icons';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import QueryTypeTag from 'components/QueryTypeTag/QueryTypeTag';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
interface PlotTagProps {
|
||||
/** Authoring mode of the panel's query; undefined when no query exists yet. */
|
||||
queryType: EQueryType | undefined;
|
||||
panelType: PANEL_TYPES;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* "Plotted with <query mode>" chip for the editor preview; V2 counterpart of V1's
|
||||
* PlotTag (duplicated per the split policy). Hidden for list panels and before a
|
||||
* query exists, where the mode is irrelevant.
|
||||
*/
|
||||
function PlotTag({
|
||||
queryType,
|
||||
panelType,
|
||||
className,
|
||||
}: PlotTagProps): JSX.Element | null {
|
||||
if (queryType === undefined || panelType === PANEL_TYPES.LIST) {
|
||||
function PlotTag({ queryType, className }: PlotTagProps): JSX.Element | null {
|
||||
if (queryType === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import PanelBody from 'pages/DashboardPage/DashboardContainer/PanelsAndSectionsL
|
||||
import PanelHeader from 'pages/DashboardPage/DashboardContainer/PanelsAndSectionsLayout/Panel/PanelHeader/PanelHeader';
|
||||
import type { AnyPanelInteractionProps } from 'pages/DashboardPage/DashboardContainer/Panels/types/interactions';
|
||||
import type { RenderablePanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelDefinition';
|
||||
import { PANEL_KIND_TO_PANEL_TYPE } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
|
||||
import type { DashboardPreference } from 'pages/DashboardPage/DashboardContainer/Panels/types/rendererProps';
|
||||
import { getPanelQueryType } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getPanelQueryType';
|
||||
import type {
|
||||
@@ -72,7 +71,6 @@ function PreviewPane({
|
||||
onClick,
|
||||
enableDrillDown,
|
||||
}: PreviewPaneProps): JSX.Element {
|
||||
const panelType = PANEL_KIND_TO_PANEL_TYPE[panel.spec.plugin.kind];
|
||||
const queryType = getPanelQueryType(panel);
|
||||
|
||||
// Search term is ephemeral preview state, threaded to header + renderer but
|
||||
@@ -84,11 +82,7 @@ function PreviewPane({
|
||||
<div className={styles.preview}>
|
||||
{!hideHeader && (
|
||||
<div className={styles.header}>
|
||||
<PlotTag
|
||||
queryType={queryType}
|
||||
panelType={panelType}
|
||||
className={styles.queryType}
|
||||
/>
|
||||
<PlotTag queryType={queryType} className={styles.queryType} />
|
||||
<div className={styles.dateTimeSelector}>
|
||||
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
|
||||
</div>
|
||||
|
||||
@@ -1,30 +1,17 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import PlotTag from '../PlotTag';
|
||||
|
||||
describe('PlotTag', () => {
|
||||
it('renders the resolved query mode', () => {
|
||||
render(
|
||||
<PlotTag queryType={EQueryType.PROM} panelType={PANEL_TYPES.TIME_SERIES} />,
|
||||
);
|
||||
render(<PlotTag queryType={EQueryType.PROM} />);
|
||||
expect(screen.getByTestId('panel-editor-plot-tag')).toBeInTheDocument();
|
||||
expect(screen.getByText('PromQL')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders nothing when there is no query yet', () => {
|
||||
render(<PlotTag queryType={undefined} panelType={PANEL_TYPES.TIME_SERIES} />);
|
||||
expect(screen.queryByTestId('panel-editor-plot-tag')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders nothing for list panels (query mode is irrelevant)', () => {
|
||||
render(
|
||||
<PlotTag
|
||||
queryType={EQueryType.QUERY_BUILDER}
|
||||
panelType={PANEL_TYPES.LIST}
|
||||
/>,
|
||||
);
|
||||
render(<PlotTag queryType={undefined} />);
|
||||
expect(screen.queryByTestId('panel-editor-plot-tag')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,10 @@ import type {
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/registry';
|
||||
import {
|
||||
getPanelDefinition,
|
||||
isPanelKindSupported,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/registry';
|
||||
import type { RenderablePanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelDefinition';
|
||||
import {
|
||||
PANEL_KIND_TO_PANEL_TYPE,
|
||||
@@ -91,8 +94,9 @@ export function usePanelEditSession({
|
||||
const query = usePanelQuery({
|
||||
panel: draft,
|
||||
panelId,
|
||||
queryCapabilities: panelDefinition.queryCapabilities,
|
||||
time,
|
||||
enabled: !!panelDefinition,
|
||||
enabled: isPanelKindSupported(panelKind),
|
||||
});
|
||||
|
||||
const { runQuery, isQueryDirty, buildSaveSpec } = usePanelEditorQuerySync({
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
DashboardtypesQueryDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import type { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
handleQueryChange,
|
||||
type PartialPanelTypes,
|
||||
@@ -146,7 +146,7 @@ export function usePanelTypeSwitch({
|
||||
);
|
||||
// Match a fresh list panel's default order so the builder's Order By isn't empty.
|
||||
const nextQuery =
|
||||
newPanelType === PANEL_TYPES.LIST
|
||||
newKind === 'signoz/ListPanel'
|
||||
? withDefaultListOrder(transformed)
|
||||
: transformed;
|
||||
const signal = getBuilderQueries(currentSpec.queries)[0]
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
Querybuildertypesv5RequestTypeDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { UNSUPPORTED_PANEL } from '../kinds/UnsupportedPanel/definition';
|
||||
import { getPanelDefinition, isPanelKindSupported } from '../registry';
|
||||
import type { PanelQueryCapabilities } from '../types/panelCapabilities';
|
||||
import { NO_PANEL_ACTIONS } from '../types/panelDefinition';
|
||||
import {
|
||||
getHiddenQueryBuilderFields,
|
||||
getSupportedQueryTypes,
|
||||
@@ -15,6 +22,7 @@ import type { PanelKind } from '../types/panelKind';
|
||||
|
||||
const { QUERY_BUILDER, CLICKHOUSE, PROM } = EQueryType;
|
||||
const { logs, traces, metrics } = TelemetrytypesSignalDTO;
|
||||
const { time_series, scalar, raw } = Querybuildertypesv5RequestTypeDTO;
|
||||
|
||||
const EXPECTED_QUERY_TYPES: Record<PanelKind, EQueryType[]> = {
|
||||
'signoz/TimeSeriesPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
@@ -37,9 +45,117 @@ const EXPECTED_SIGNALS: Record<PanelKind, TelemetrytypesSignalDTO[]> = {
|
||||
'signoz/ListPanel': [logs, traces],
|
||||
};
|
||||
|
||||
// Exhaustive over PanelKind, so a new kind can't ship without stating how its request is
|
||||
// shaped — the check that used to be implicit in a legacy PANEL_TYPES switch.
|
||||
const EXPECTED_QUERY_CAPABILITIES: Record<PanelKind, PanelQueryCapabilities> = {
|
||||
'signoz/TimeSeriesPanel': {
|
||||
requestType: time_series,
|
||||
formatTableResultForUI: false,
|
||||
bucketedStepInterval: false,
|
||||
orderTiebreaker: false,
|
||||
serverPaginated: false,
|
||||
},
|
||||
// Bar bins client-side, so it asks for a widened step interval over a raw series.
|
||||
'signoz/BarChartPanel': {
|
||||
requestType: time_series,
|
||||
formatTableResultForUI: false,
|
||||
bucketedStepInterval: true,
|
||||
orderTiebreaker: false,
|
||||
serverPaginated: false,
|
||||
},
|
||||
'signoz/HistogramPanel': {
|
||||
requestType: time_series,
|
||||
formatTableResultForUI: false,
|
||||
bucketedStepInterval: false,
|
||||
orderTiebreaker: false,
|
||||
serverPaginated: false,
|
||||
},
|
||||
'signoz/NumberPanel': {
|
||||
requestType: scalar,
|
||||
formatTableResultForUI: false,
|
||||
bucketedStepInterval: false,
|
||||
orderTiebreaker: false,
|
||||
serverPaginated: false,
|
||||
},
|
||||
'signoz/PieChartPanel': {
|
||||
requestType: scalar,
|
||||
formatTableResultForUI: false,
|
||||
bucketedStepInterval: false,
|
||||
orderTiebreaker: false,
|
||||
serverPaginated: false,
|
||||
},
|
||||
// Only Table asks the server to transpose its scalar result into UI rows.
|
||||
'signoz/TablePanel': {
|
||||
requestType: scalar,
|
||||
formatTableResultForUI: true,
|
||||
bucketedStepInterval: false,
|
||||
orderTiebreaker: false,
|
||||
serverPaginated: false,
|
||||
},
|
||||
// Only List reads raw rows, pages them server-side, and needs an order tiebreaker.
|
||||
'signoz/ListPanel': {
|
||||
requestType: raw,
|
||||
formatTableResultForUI: false,
|
||||
bucketedStepInterval: false,
|
||||
orderTiebreaker: true,
|
||||
serverPaginated: true,
|
||||
},
|
||||
};
|
||||
|
||||
const ALL_KINDS = Object.keys(EXPECTED_QUERY_TYPES) as PanelKind[];
|
||||
|
||||
describe('panel capabilities guard', () => {
|
||||
describe('query capabilities', () => {
|
||||
it.each(ALL_KINDS)('declares how %s shapes its request', (kind) => {
|
||||
expect(getPanelDefinition(kind).queryCapabilities).toStrictEqual(
|
||||
EXPECTED_QUERY_CAPABILITIES[kind],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// A dashboard spec written by a newer SigNoz can name a kind this build has no
|
||||
// definition for. The registry answers with UNSUPPORTED_PANEL rather than nothing, so
|
||||
// every guard below reads it without first proving a definition exists.
|
||||
describe('a kind this build cannot render', () => {
|
||||
const unknownKind = 'signoz/SomeFutureKindPanel' as PanelKind;
|
||||
|
||||
it('is not reported as supported', () => {
|
||||
expect(isPanelKindSupported(unknownKind)).toBe(false);
|
||||
expect(isPanelKindSupported('signoz/TimeSeriesPanel')).toBe(true);
|
||||
});
|
||||
|
||||
it('still resolves to a definition', () => {
|
||||
expect(getPanelDefinition(unknownKind)).toBe(UNSUPPORTED_PANEL);
|
||||
});
|
||||
|
||||
it('declares nothing, so it is never offered as authorable', () => {
|
||||
expect(getSupportedSignals(unknownKind)).toStrictEqual([]);
|
||||
expect(getSupportedQueryTypes(unknownKind)).toStrictEqual([]);
|
||||
expect(isSignalSupported(unknownKind, logs)).toBe(false);
|
||||
expect(
|
||||
isPanelCombinationValid({ kind: unknownKind, queryType: QUERY_BUILDER }),
|
||||
).toBe(false);
|
||||
expect(getHiddenQueryBuilderFields(unknownKind, logs)).toStrictEqual({});
|
||||
expect(getPanelDefinition(unknownKind).sections).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('offers no actions', () => {
|
||||
expect(getPanelDefinition(unknownKind).actions).toStrictEqual(
|
||||
NO_PANEL_ACTIONS,
|
||||
);
|
||||
expect(NO_PANEL_ACTIONS.view).toBe(false);
|
||||
expect(NO_PANEL_ACTIONS.edit).toBe(false);
|
||||
expect(NO_PANEL_ACTIONS.drilldown).toBe(false);
|
||||
});
|
||||
|
||||
it('carries an inert query shape, so a stray request can do no harm', () => {
|
||||
const { queryCapabilities } = getPanelDefinition(unknownKind);
|
||||
expect(queryCapabilities.requestType).toBe(time_series);
|
||||
expect(queryCapabilities.serverPaginated).toBe(false);
|
||||
expect(queryCapabilities.formatTableResultForUI).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('query type support', () => {
|
||||
it.each(ALL_KINDS)('declares the expected query types for %s', (kind) => {
|
||||
expect(getSupportedQueryTypes(kind)).toStrictEqual(
|
||||
|
||||
@@ -20,8 +20,12 @@ interface NoDataProps {
|
||||
isFetching?: boolean;
|
||||
/** When provided, renders a Retry button that re-runs the query. */
|
||||
onRetry?: () => void;
|
||||
/** Hides the global "Extend time range" action when this panel is locked to a fixed time preference. */
|
||||
panel?: DashboardtypesPanelDTO;
|
||||
/**
|
||||
* The panel this empty state stands in for. Every renderer has it, and it decides
|
||||
* whether the global "Extend time range" action applies (a panel locked to a fixed
|
||||
* time preference can't be widened by it) as well as what the action events report.
|
||||
*/
|
||||
panel: DashboardtypesPanelDTO;
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
@@ -43,19 +47,17 @@ function NoData({
|
||||
const globalExtend = useExtendTimeWindow();
|
||||
// The View modal's local extender wins; the global one only applies to a panel that
|
||||
// follows the ambient window (a fixed preference can't be widened by it).
|
||||
const hasFixedTimePreference = panel
|
||||
? panelHasFixedTimePreference(panel)
|
||||
: false;
|
||||
const activeExtend =
|
||||
viewExtend ?? (hasFixedTimePreference ? undefined : globalExtend);
|
||||
viewExtend ?? (panelHasFixedTimePreference(panel) ? undefined : globalExtend);
|
||||
|
||||
if (isFetching) {
|
||||
return <PanelLoader />;
|
||||
}
|
||||
|
||||
const panelType = panel
|
||||
? PANEL_KIND_TO_PANEL_TYPE[panel.spec.plugin.kind]
|
||||
: undefined;
|
||||
// `panelType` stays on the event so existing reports keep resolving; `panelKind` is the
|
||||
// V2 identity, and the only one that can tell two kinds sharing a panel type apart.
|
||||
const panelKind = panel.spec.plugin.kind;
|
||||
const panelType = PANEL_KIND_TO_PANEL_TYPE[panelKind];
|
||||
|
||||
const extendAction: PanelMessageAction | undefined =
|
||||
activeExtend?.canExtend && activeExtend.actionLabel
|
||||
@@ -65,6 +67,7 @@ function NoData({
|
||||
void logEvent(DashboardDetailEvents.NoDataAction, {
|
||||
action: 'extendTime',
|
||||
panelType,
|
||||
panelKind,
|
||||
});
|
||||
activeExtend.extend();
|
||||
},
|
||||
@@ -79,6 +82,7 @@ function NoData({
|
||||
void logEvent(DashboardDetailEvents.NoDataAction, {
|
||||
action: 'retry',
|
||||
panelType,
|
||||
panelKind,
|
||||
});
|
||||
onRetry();
|
||||
},
|
||||
|
||||
@@ -33,7 +33,12 @@ function panelWith(
|
||||
timePreference?: DashboardtypesTimePreferenceDTO,
|
||||
): DashboardtypesPanelDTO {
|
||||
return {
|
||||
spec: { plugin: { spec: { visualization: { timePreference } } } },
|
||||
spec: {
|
||||
plugin: {
|
||||
kind: 'signoz/TimeSeriesPanel',
|
||||
spec: { visualization: { timePreference } },
|
||||
},
|
||||
},
|
||||
} as unknown as DashboardtypesPanelDTO;
|
||||
}
|
||||
|
||||
@@ -44,7 +49,7 @@ describe('NoData', () => {
|
||||
});
|
||||
|
||||
it('renders the empty-state title and hint', () => {
|
||||
render(<NoData />);
|
||||
render(<NoData panel={panelWith()} />);
|
||||
|
||||
expect(screen.getByTestId('panel-no-data')).toBeInTheDocument();
|
||||
expect(screen.getByText('No data in this time range')).toBeInTheDocument();
|
||||
@@ -55,7 +60,7 @@ describe('NoData', () => {
|
||||
|
||||
it('offers to extend the window as the primary action', () => {
|
||||
mockUseExtendTimeWindow.mockReturnValue(extender());
|
||||
render(<NoData />);
|
||||
render(<NoData panel={panelWith()} />);
|
||||
|
||||
const action = screen.getByTestId('panel-no-data-action');
|
||||
expect(action).toHaveTextContent('Extend time range');
|
||||
@@ -68,7 +73,7 @@ describe('NoData', () => {
|
||||
it('renders both Extend (primary) and Retry (secondary) when a retry handler is given', () => {
|
||||
const onRetry = jest.fn();
|
||||
mockUseExtendTimeWindow.mockReturnValue(extender());
|
||||
render(<NoData onRetry={onRetry} />);
|
||||
render(<NoData onRetry={onRetry} panel={panelWith()} />);
|
||||
|
||||
expect(screen.getByTestId('panel-no-data-action')).toHaveTextContent(
|
||||
'Extend time range',
|
||||
@@ -82,7 +87,7 @@ describe('NoData', () => {
|
||||
|
||||
it('falls back to Retry as the sole action when the window cannot be widened', () => {
|
||||
const onRetry = jest.fn();
|
||||
render(<NoData onRetry={onRetry} />);
|
||||
render(<NoData onRetry={onRetry} panel={panelWith()} />);
|
||||
|
||||
const action = screen.getByTestId('panel-no-data-action');
|
||||
expect(action).toHaveTextContent('Retry');
|
||||
@@ -101,7 +106,7 @@ describe('NoData', () => {
|
||||
useViewPanelStore.setState({
|
||||
viewPanelExtendWindow: extender({ extend: storeExtend }),
|
||||
});
|
||||
render(<NoData />);
|
||||
render(<NoData panel={panelWith()} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('panel-no-data-action'));
|
||||
expect(storeExtend).toHaveBeenCalledTimes(1);
|
||||
@@ -109,7 +114,7 @@ describe('NoData', () => {
|
||||
});
|
||||
|
||||
it('renders no action when nothing can be widened and no retry handler', () => {
|
||||
render(<NoData />);
|
||||
render(<NoData panel={panelWith()} />);
|
||||
|
||||
expect(screen.queryByTestId('panel-no-data-action')).not.toBeInTheDocument();
|
||||
expect(
|
||||
@@ -119,7 +124,7 @@ describe('NoData', () => {
|
||||
|
||||
it('shows the panel loader (not the empty state) while refetching', () => {
|
||||
mockUseExtendTimeWindow.mockReturnValue(extender());
|
||||
render(<NoData isFetching />);
|
||||
render(<NoData isFetching panel={panelWith()} />);
|
||||
|
||||
expect(screen.getByTestId('panel-loading')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('panel-no-data')).not.toBeInTheDocument();
|
||||
@@ -128,7 +133,7 @@ describe('NoData', () => {
|
||||
|
||||
it('honours the data-testid override for the number panel', () => {
|
||||
mockUseExtendTimeWindow.mockReturnValue(extender());
|
||||
render(<NoData data-testid="number-panel-no-data" />);
|
||||
render(<NoData data-testid="number-panel-no-data" panel={panelWith()} />);
|
||||
|
||||
expect(screen.getByTestId('number-panel-no-data')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
Querybuildertypesv5RequestTypeDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
export const definition: PanelDefinition<'signoz/BarChartPanel'> = {
|
||||
@@ -20,6 +23,15 @@ export const definition: PanelDefinition<'signoz/BarChartPanel'> = {
|
||||
EQueryType.PROM,
|
||||
],
|
||||
queryBuilderFields: {},
|
||||
// Bars are binned client-side from a raw time series, so the request asks for a
|
||||
// step interval wide enough to keep the bar count readable (V1 parity).
|
||||
queryCapabilities: {
|
||||
requestType: Querybuildertypesv5RequestTypeDTO.time_series,
|
||||
formatTableResultForUI: false,
|
||||
bucketedStepInterval: true,
|
||||
orderTiebreaker: false,
|
||||
serverPaginated: false,
|
||||
},
|
||||
actions: {
|
||||
view: true,
|
||||
edit: true,
|
||||
|
||||
@@ -1,33 +1,22 @@
|
||||
import type { DashboardtypesBarChartPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PanelMode } from 'lib/visualization/panels/types';
|
||||
import { buildBaseConfig } from 'pages/DashboardPage/DashboardContainer/Panels/utils/baseConfigBuilder';
|
||||
import {
|
||||
buildBaseConfig,
|
||||
type TimeAxisChromeArgs,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/utils/baseConfigBuilder';
|
||||
import { resolveSeriesLabelV5 } from 'pages/DashboardPage/DashboardContainer/Panels/utils/resolveSeriesLabel';
|
||||
import type { PanelSeries } from 'pages/DashboardPage/DashboardContainer/queryV5/types';
|
||||
import { toClickPluginPayload } from 'pages/DashboardPage/DashboardContainer/queryV5/uplotData';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { DrawStyle } from 'lib/uPlotV2/config/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import type { BuilderQuery } from 'types/api/v5/queryRange';
|
||||
|
||||
export interface BuildBarChartConfigArgs {
|
||||
panelId: string;
|
||||
export interface BuildBarChartConfigArgs extends TimeAxisChromeArgs {
|
||||
spec: DashboardtypesBarChartPanelSpecDTO;
|
||||
/** Flat list of builder queries (see `getBuilderQueries`); powers per-query legend resolution. */
|
||||
builderQueries: BuilderQuery[];
|
||||
/** Flattened V5 series (see `flattenTimeSeries`). */
|
||||
series: PanelSeries[];
|
||||
/** Per-query step intervals from the response exec stats. */
|
||||
stepIntervals?: Record<string, number>;
|
||||
isDarkMode: boolean;
|
||||
timezone: Timezone;
|
||||
panelMode: PanelMode;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
onClick?: OnClickPluginOpts['onClick'];
|
||||
minTimeScale?: number;
|
||||
maxTimeScale?: number;
|
||||
}
|
||||
|
||||
/** Builds a `UPlotConfigBuilder` for a Bar chart panel: shared scaffolding, optional stacking, one bar series per result. */
|
||||
@@ -47,7 +36,7 @@ export function buildBarChartConfig({
|
||||
}: BuildBarChartConfigArgs): UPlotConfigBuilder {
|
||||
const builder = buildBaseConfig({
|
||||
panelId,
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
isTimeAxis: true,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
panelMode,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
Querybuildertypesv5RequestTypeDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
export const definition: PanelDefinition<'signoz/HistogramPanel'> = {
|
||||
@@ -20,6 +23,15 @@ export const definition: PanelDefinition<'signoz/HistogramPanel'> = {
|
||||
EQueryType.PROM,
|
||||
],
|
||||
queryBuilderFields: {},
|
||||
// Buckets are computed client-side from the raw series, so the request is a plain
|
||||
// time series — the bucket count is a display concern, not a query one.
|
||||
queryCapabilities: {
|
||||
requestType: Querybuildertypesv5RequestTypeDTO.time_series,
|
||||
formatTableResultForUI: false,
|
||||
bucketedStepInterval: false,
|
||||
orderTiebreaker: false,
|
||||
serverPaginated: false,
|
||||
},
|
||||
actions: {
|
||||
view: true,
|
||||
edit: true,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { DashboardtypesHistogramPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PanelMode } from 'lib/visualization/panels/types';
|
||||
import { buildBaseConfig } from 'pages/DashboardPage/DashboardContainer/Panels/utils/baseConfigBuilder';
|
||||
import {
|
||||
buildBaseConfig,
|
||||
type PanelChromeArgs,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/utils/baseConfigBuilder';
|
||||
import { resolveSeriesLabelV5 } from 'pages/DashboardPage/DashboardContainer/Panels/utils/resolveSeriesLabel';
|
||||
import type { PanelSeries } from 'pages/DashboardPage/DashboardContainer/queryV5/types';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
@@ -16,16 +16,12 @@ const BAR_WIDTH_FACTOR = 1;
|
||||
const MERGED_SERIES_LINE_COLOR = '#3f5ecc';
|
||||
const MERGED_SERIES_FILL_COLOR = '#4E74F8';
|
||||
|
||||
export interface BuildHistogramConfigArgs {
|
||||
panelId: string;
|
||||
export interface BuildHistogramConfigArgs extends PanelChromeArgs {
|
||||
spec: DashboardtypesHistogramPanelSpecDTO;
|
||||
/** Builder queries on this panel — used to resolve per-series labels. */
|
||||
builderQueries: BuilderQuery[];
|
||||
/** Flattened V5 series (see `flattenTimeSeries`). */
|
||||
series: PanelSeries[];
|
||||
isDarkMode: boolean;
|
||||
timezone: Timezone;
|
||||
panelMode: PanelMode;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,7 +40,7 @@ export function buildHistogramConfig({
|
||||
}: BuildHistogramConfigArgs): UPlotConfigBuilder {
|
||||
const builder = buildBaseConfig({
|
||||
panelId,
|
||||
panelType: PANEL_TYPES.HISTOGRAM,
|
||||
isTimeAxis: false,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
panelMode,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
Querybuildertypesv5RequestTypeDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
@@ -30,6 +33,15 @@ export const definition: PanelDefinition<'signoz/ListPanel'> = {
|
||||
},
|
||||
},
|
||||
sections,
|
||||
// The only kind reading raw rows: they page server-side, and the sort needs a
|
||||
// tiebreaker so a duplicated sort key can't repeat or skip a row across pages.
|
||||
queryCapabilities: {
|
||||
requestType: Querybuildertypesv5RequestTypeDTO.raw,
|
||||
formatTableResultForUI: false,
|
||||
bucketedStepInterval: false,
|
||||
orderTiebreaker: true,
|
||||
serverPaginated: true,
|
||||
},
|
||||
actions: {
|
||||
view: true,
|
||||
edit: true,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
Querybuildertypesv5RequestTypeDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
export const definition: PanelDefinition<'signoz/NumberPanel'> = {
|
||||
@@ -20,6 +23,13 @@ export const definition: PanelDefinition<'signoz/NumberPanel'> = {
|
||||
EQueryType.PROM,
|
||||
],
|
||||
queryBuilderFields: {},
|
||||
queryCapabilities: {
|
||||
requestType: Querybuildertypesv5RequestTypeDTO.scalar,
|
||||
formatTableResultForUI: false,
|
||||
bucketedStepInterval: false,
|
||||
orderTiebreaker: false,
|
||||
serverPaginated: false,
|
||||
},
|
||||
actions: {
|
||||
view: true,
|
||||
edit: true,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
Querybuildertypesv5RequestTypeDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
export const definition: PanelDefinition<'signoz/PieChartPanel'> = {
|
||||
@@ -16,6 +19,13 @@ export const definition: PanelDefinition<'signoz/PieChartPanel'> = {
|
||||
],
|
||||
supportedQueryTypes: [EQueryType.QUERY_BUILDER, EQueryType.CLICKHOUSE],
|
||||
queryBuilderFields: {},
|
||||
queryCapabilities: {
|
||||
requestType: Querybuildertypesv5RequestTypeDTO.scalar,
|
||||
formatTableResultForUI: false,
|
||||
bucketedStepInterval: false,
|
||||
orderTiebreaker: false,
|
||||
serverPaginated: false,
|
||||
},
|
||||
actions: {
|
||||
view: true,
|
||||
edit: true,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
Querybuildertypesv5RequestTypeDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
export const definition: PanelDefinition<'signoz/TablePanel'> = {
|
||||
@@ -16,6 +19,14 @@ export const definition: PanelDefinition<'signoz/TablePanel'> = {
|
||||
],
|
||||
supportedQueryTypes: [EQueryType.QUERY_BUILDER, EQueryType.CLICKHOUSE],
|
||||
queryBuilderFields: {},
|
||||
// The only kind that asks the server to transpose its scalar result into UI rows.
|
||||
queryCapabilities: {
|
||||
requestType: Querybuildertypesv5RequestTypeDTO.scalar,
|
||||
formatTableResultForUI: true,
|
||||
bucketedStepInterval: false,
|
||||
orderTiebreaker: false,
|
||||
serverPaginated: false,
|
||||
},
|
||||
// Tables carry tabular data worth exporting (V1 parity: download is table-only).
|
||||
actions: {
|
||||
view: true,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
Querybuildertypesv5RequestTypeDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
export const definition: PanelDefinition<'signoz/TimeSeriesPanel'> = {
|
||||
@@ -20,6 +23,13 @@ export const definition: PanelDefinition<'signoz/TimeSeriesPanel'> = {
|
||||
EQueryType.PROM,
|
||||
],
|
||||
queryBuilderFields: {},
|
||||
queryCapabilities: {
|
||||
requestType: Querybuildertypesv5RequestTypeDTO.time_series,
|
||||
formatTableResultForUI: false,
|
||||
bucketedStepInterval: false,
|
||||
orderTiebreaker: false,
|
||||
serverPaginated: false,
|
||||
},
|
||||
actions: {
|
||||
view: true,
|
||||
edit: true,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import type { DashboardtypesTimeSeriesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PanelMode } from 'lib/visualization/panels/types';
|
||||
import {
|
||||
buildBaseConfig,
|
||||
minStepInterval,
|
||||
type TimeAxisChromeArgs,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/utils/baseConfigBuilder';
|
||||
import {
|
||||
FILL_MODE_MAP,
|
||||
@@ -19,7 +17,6 @@ import {
|
||||
toClickPluginPayload,
|
||||
} from 'pages/DashboardPage/DashboardContainer/queryV5/uplotData';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import {
|
||||
DrawStyle,
|
||||
FillMode,
|
||||
@@ -31,22 +28,12 @@ import type { BuilderQuery } from 'types/api/v5/queryRange';
|
||||
|
||||
const DEFAULT_POINT_SIZE = 5;
|
||||
|
||||
export interface BuildTimeSeriesConfigArgs {
|
||||
panelId: string;
|
||||
export interface BuildTimeSeriesConfigArgs extends TimeAxisChromeArgs {
|
||||
spec: DashboardtypesTimeSeriesPanelSpecDTO;
|
||||
/** Flat list of builder queries (see `getBuilderQueries`); powers per-query legend resolution. */
|
||||
builderQueries: BuilderQuery[];
|
||||
/** Flattened V5 series (see `flattenTimeSeries`). */
|
||||
series: PanelSeries[];
|
||||
/** Per-query step intervals from the response exec stats. */
|
||||
stepIntervals?: Record<string, number>;
|
||||
isDarkMode: boolean;
|
||||
timezone: Timezone;
|
||||
panelMode: PanelMode;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
onClick?: OnClickPluginOpts['onClick'];
|
||||
minTimeScale?: number;
|
||||
maxTimeScale?: number;
|
||||
}
|
||||
|
||||
/** Builds a `UPlotConfigBuilder` for a TimeSeries panel: shared scaffolding plus one series per result. */
|
||||
@@ -66,7 +53,7 @@ export function buildTimeSeriesConfig({
|
||||
}: BuildTimeSeriesConfigArgs): UPlotConfigBuilder {
|
||||
const builder = buildBaseConfig({
|
||||
panelId,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
isTimeAxis: true,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
panelMode,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { CircleHelp } from '@signozhq/icons';
|
||||
|
||||
import PanelMessage from '../../components/PanelMessage/PanelMessage';
|
||||
import PanelStyles from '../../panel.module.scss';
|
||||
|
||||
/**
|
||||
* Body for a panel whose kind this build has no renderer for — a spec written by a newer
|
||||
* SigNoz names a visualization that didn't exist when this client shipped. Says so in
|
||||
* place of the chart, so the panel keeps its slot in the layout instead of leaving a hole.
|
||||
*/
|
||||
function UnsupportedPanelRenderer(): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
data-testid="unsupported-panel-renderer"
|
||||
className={PanelStyles.panelContainer}
|
||||
>
|
||||
<PanelMessage
|
||||
icon={<CircleHelp size={18} />}
|
||||
title="Unsupported panel type"
|
||||
description="This panel was built with a newer version of SigNoz. Upgrade to view it."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UnsupportedPanelRenderer;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Querybuildertypesv5RequestTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import {
|
||||
NO_PANEL_ACTIONS,
|
||||
type RenderablePanelDefinition,
|
||||
} from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
|
||||
/**
|
||||
* Stand-in definition for a kind that isn't in the registry, so `getPanelDefinition`
|
||||
* always resolves and no caller has to branch on a missing one. It declares nothing: no
|
||||
* signals, no query types, no config sections and no actions — an unknown kind can't be
|
||||
* queried, configured or acted on, only shown as unsupported.
|
||||
*
|
||||
* `kind` carries a sentinel that no API enum value can collide with; the cast is the one
|
||||
* place this definition steps outside `PanelKind`.
|
||||
*/
|
||||
export const UNSUPPORTED_PANEL: RenderablePanelDefinition = {
|
||||
kind: '<unsupported>' as RenderablePanelDefinition['kind'],
|
||||
displayName: 'Unsupported panel',
|
||||
Renderer,
|
||||
sections: [],
|
||||
supportedSignals: [],
|
||||
supportedQueryTypes: [],
|
||||
queryBuilderFields: {},
|
||||
queryCapabilities: {
|
||||
requestType: Querybuildertypesv5RequestTypeDTO.time_series,
|
||||
formatTableResultForUI: false,
|
||||
bucketedStepInterval: false,
|
||||
orderTiebreaker: false,
|
||||
serverPaginated: false,
|
||||
},
|
||||
actions: NO_PANEL_ACTIONS,
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { definition as PieChart } from './kinds/PieChartPanel/definition';
|
||||
import { definition as TimeSeries } from './kinds/TimeSeriesPanel/definition';
|
||||
import { definition as Table } from './kinds/TablePanel/definition';
|
||||
import { definition as List } from './kinds/ListPanel/definition';
|
||||
import { UNSUPPORTED_PANEL } from './kinds/UnsupportedPanel/definition';
|
||||
import type {
|
||||
PanelRegistry,
|
||||
RenderablePanelDefinition,
|
||||
@@ -22,8 +23,24 @@ export const PANELS: PanelRegistry = {
|
||||
[List.kind]: List,
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether this build can render the kind. `PanelKind` spans every kind the API declares,
|
||||
* but a dashboard spec written by a newer SigNoz can name one this client has never heard
|
||||
* of — so ask before doing work on a panel's behalf, such as fetching its data.
|
||||
*/
|
||||
export function isPanelKindSupported(kind: PanelKind): boolean {
|
||||
return kind in PANELS;
|
||||
}
|
||||
|
||||
/**
|
||||
* The definition for a kind — always one. An unregistered kind resolves to
|
||||
* {@link UNSUPPORTED_PANEL}, which declares no capabilities and renders as unsupported, so
|
||||
* callers read a definition's fields without first proving it exists.
|
||||
*/
|
||||
export function getPanelDefinition(kind: PanelKind): RenderablePanelDefinition {
|
||||
// Single intentional cast widening the per-kind Renderer to the kind-agnostic
|
||||
// prop surface (a per-kind renderer can't be statically validated against the union).
|
||||
return PANELS[kind] as RenderablePanelDefinition;
|
||||
return (
|
||||
(PANELS[kind] as RenderablePanelDefinition | undefined) ?? UNSUPPORTED_PANEL
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
Querybuildertypesv5RequestTypeDTO,
|
||||
type TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
|
||||
/**
|
||||
@@ -18,3 +21,30 @@ export type FilterConfigsPartial = NonNullable<
|
||||
export type QueryBuilderFieldRule = {
|
||||
default?: FilterConfigsPartial;
|
||||
} & Partial<Record<TelemetrytypesSignalDTO, FilterConfigsPartial>>;
|
||||
|
||||
/**
|
||||
* How a kind's query-range request is shaped. Declared per-kind in
|
||||
* `kinds/<Kind>/definition.ts` and read through the capabilities guard, so no V2 code
|
||||
* has to translate a panel kind into the legacy `PANEL_TYPES` enum to answer these.
|
||||
*/
|
||||
export interface PanelQueryCapabilities {
|
||||
/** V5 request type the panel's data comes back as. */
|
||||
requestType: Querybuildertypesv5RequestTypeDTO;
|
||||
/** Server transposes the scalar result into UI table rows (`formatOptions.formatTableResultForUI`). */
|
||||
formatTableResultForUI: boolean;
|
||||
/**
|
||||
* Widen the step interval to cap how many buckets come back — kinds that bin
|
||||
* client-side from a raw time series rather than plotting every point.
|
||||
*/
|
||||
bucketedStepInterval: boolean;
|
||||
/**
|
||||
* Append a deterministic tiebreaker to the query's `order` so offset paging over raw
|
||||
* rows can't repeat or skip a row when the sort key has duplicates.
|
||||
*/
|
||||
orderTiebreaker: boolean;
|
||||
/**
|
||||
* Rows page server-side via `offset`/`limit`. AND-ed at the call site with "the query
|
||||
* carries no explicit limit" — an explicit limit means the user asked for a fixed set.
|
||||
*/
|
||||
serverPaginated: boolean;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ import type { EQueryType } from 'types/common/dashboard';
|
||||
import type { SectionConfig } from './sections';
|
||||
import type { AnyPanelInteractionProps } from './interactions';
|
||||
import type { PanelKind } from './panelKind';
|
||||
import type { QueryBuilderFieldRule } from './panelCapabilities';
|
||||
import type {
|
||||
PanelQueryCapabilities,
|
||||
QueryBuilderFieldRule,
|
||||
} from './panelCapabilities';
|
||||
import type { BaseRendererProps, PanelRendererProps } from './rendererProps';
|
||||
|
||||
/** Export formats offered under the single "Download" action. */
|
||||
@@ -39,6 +42,24 @@ export interface PanelActionCapabilities {
|
||||
drilldown: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* No actions at all — for a kind this build can't render, where every action would act on
|
||||
* a panel body that isn't there. See `UNSUPPORTED_PANEL`.
|
||||
*/
|
||||
export const NO_PANEL_ACTIONS: PanelActionCapabilities = {
|
||||
view: false,
|
||||
edit: false,
|
||||
clone: false,
|
||||
download: {
|
||||
[DownloadFormat.CSV]: false,
|
||||
[DownloadFormat.PNG]: false,
|
||||
[DownloadFormat.SVG]: false,
|
||||
},
|
||||
createAlert: false,
|
||||
search: false,
|
||||
drilldown: false,
|
||||
};
|
||||
|
||||
export interface PanelDefinition<K extends PanelKind = PanelKind> {
|
||||
kind: K;
|
||||
displayName: string;
|
||||
@@ -50,6 +71,8 @@ export interface PanelDefinition<K extends PanelKind = PanelKind> {
|
||||
supportedQueryTypes: EQueryType[];
|
||||
/** Query-builder fields this kind hides/disables, optionally per signal (`{}` hides none). */
|
||||
queryBuilderFields: QueryBuilderFieldRule;
|
||||
/** How this kind's query-range request is shaped (request type, paging, result formatting). */
|
||||
queryCapabilities: PanelQueryCapabilities;
|
||||
actions: PanelActionCapabilities;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { buildDefaultQueries } from '../buildDefaultQueries';
|
||||
|
||||
describe('buildDefaultQueries', () => {
|
||||
it('seeds a List panel with a runnable logs query ordered by timestamp desc', () => {
|
||||
it('seeds a list panel with a runnable logs query ordered by timestamp desc', () => {
|
||||
const queries = buildDefaultQueries('signoz/ListPanel');
|
||||
|
||||
expect(queries).toHaveLength(1);
|
||||
@@ -13,7 +13,7 @@ describe('buildDefaultQueries', () => {
|
||||
expect(serialized.toLowerCase()).toContain('logs');
|
||||
});
|
||||
|
||||
it('seeds a List panel without a limit so it pages server-side by default', () => {
|
||||
it('seeds a list panel without a limit so it pages server-side by default', () => {
|
||||
const queries = buildDefaultQueries('signoz/ListPanel');
|
||||
|
||||
// A limit would make usePanelQuery treat the panel as a static, unpaged list.
|
||||
@@ -21,7 +21,7 @@ describe('buildDefaultQueries', () => {
|
||||
expect(spec.limit).toBeUndefined();
|
||||
});
|
||||
|
||||
it('seeds no query for non-List kinds (they seed from the builder)', () => {
|
||||
it('seeds no query for plotted kinds (they seed from the builder)', () => {
|
||||
expect(buildDefaultQueries('signoz/TimeSeriesPanel')).toStrictEqual([]);
|
||||
expect(buildDefaultQueries('signoz/NumberPanel')).toStrictEqual([]);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import type {
|
||||
DashboardtypesThresholdWithLabelDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PanelMode } from 'lib/visualization/panels/types';
|
||||
import onClickPlugin, {
|
||||
OnClickPluginOpts,
|
||||
@@ -26,7 +25,11 @@ import {
|
||||
*/
|
||||
export interface BuildBaseConfigArgs {
|
||||
panelId: string;
|
||||
panelType: PANEL_TYPES;
|
||||
/**
|
||||
* X axis plots timestamps, so its ticks format as dates/times. Each kind states this
|
||||
* for itself — a bucketed x axis (histogram) passes false.
|
||||
*/
|
||||
isTimeAxis: boolean;
|
||||
isDarkMode: boolean;
|
||||
timezone: Timezone;
|
||||
panelMode: PanelMode;
|
||||
@@ -56,6 +59,18 @@ export interface BuildBaseConfigArgs {
|
||||
onClick?: OnClickPluginOpts['onClick'];
|
||||
}
|
||||
|
||||
/** What a kind's build args pass straight through; the rest is derived from its spec. */
|
||||
export type PanelChromeArgs = Pick<
|
||||
BuildBaseConfigArgs,
|
||||
'panelId' | 'isDarkMode' | 'timezone' | 'panelMode'
|
||||
>;
|
||||
|
||||
export type TimeAxisChromeArgs = PanelChromeArgs &
|
||||
Pick<
|
||||
BuildBaseConfigArgs,
|
||||
'stepIntervals' | 'minTimeScale' | 'maxTimeScale' | 'onDragSelect' | 'onClick'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Builds the panel-agnostic scaffolding of a uPlot chart (scales, thresholds,
|
||||
* axes, drag-to-zoom, click plugin). Callers then `addSeries`/`addPlugin` on the
|
||||
@@ -63,7 +78,7 @@ export interface BuildBaseConfigArgs {
|
||||
*/
|
||||
export function buildBaseConfig({
|
||||
panelId,
|
||||
panelType,
|
||||
isTimeAxis,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
panelMode,
|
||||
@@ -133,7 +148,7 @@ export function buildBaseConfig({
|
||||
side: 2,
|
||||
isDarkMode,
|
||||
isLogScale,
|
||||
panelType,
|
||||
isTimeAxis,
|
||||
});
|
||||
|
||||
builder.addAxis({
|
||||
@@ -143,7 +158,6 @@ export function buildBaseConfig({
|
||||
isDarkMode,
|
||||
isLogScale,
|
||||
yAxisUnit,
|
||||
panelType,
|
||||
});
|
||||
|
||||
return builder;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import type { DashboardtypesQueryDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { listViewInitialLogQuery, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { listViewInitialLogQuery } from 'constants/queryBuilder';
|
||||
|
||||
import { toPerses } from '../../queryV5/persesQueryAdapters';
|
||||
import { PANEL_KIND_TO_PANEL_TYPE, type PanelKind } from '../types/panelKind';
|
||||
|
||||
/** Seed query for a new panel. Only List needs one (logs, timestamp desc) so its
|
||||
/** Seed query for a new panel. Only a list panel needs one (logs, timestamp desc) so its
|
||||
* preview runs on open; other kinds start empty and seed from the builder. */
|
||||
export function buildDefaultQueries(kind: PanelKind): DashboardtypesQueryDTO[] {
|
||||
if (PANEL_KIND_TO_PANEL_TYPE[kind] === PANEL_TYPES.LIST) {
|
||||
return toPerses(listViewInitialLogQuery, PANEL_TYPES.LIST);
|
||||
if (kind !== 'signoz/ListPanel') {
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
// `toPerses` pivots through the V1 `Query`, which is still keyed by panel type.
|
||||
return toPerses(listViewInitialLogQuery, PANEL_KIND_TO_PANEL_TYPE[kind]);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import ContextMenu from 'periscope/components/ContextMenu';
|
||||
import { getPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/registry';
|
||||
import {
|
||||
getPanelDefinition,
|
||||
isPanelKindSupported,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/registry';
|
||||
import {
|
||||
getPanelTimePreference,
|
||||
panelTimePreferenceLabel,
|
||||
@@ -50,15 +53,22 @@ function Panel({
|
||||
|
||||
// Header search: only kinds that declare it render the box. The term is owned
|
||||
// here and threaded to both the header (input) and renderer (filter).
|
||||
const searchable = !!panelDefinition?.actions.search;
|
||||
const searchable = panelDefinition.actions.search;
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
// Only an explicit false defers the fetch: `isVisible` is undefined wherever no
|
||||
// observer reports visibility (the View modal, the editor preview), and those panels
|
||||
// are on screen by construction.
|
||||
const isOffScreen = isVisible === false;
|
||||
|
||||
const { data, isFetching, isPreviousData, error, refetch, pagination } =
|
||||
usePanelQuery({
|
||||
panel,
|
||||
panelId,
|
||||
// Lazy: fetch only once on screen (undefined → visible) and a renderer exists.
|
||||
enabled: !!panelDefinition && isVisible !== false,
|
||||
queryCapabilities: panelDefinition.queryCapabilities,
|
||||
// Lazy: fetch once on screen, and never for a kind this build can't render —
|
||||
// the data would have nothing to render into.
|
||||
enabled: isPanelKindSupported(panelKind) && !isOffScreen,
|
||||
});
|
||||
|
||||
const { onDragSelect, dashboardPreference } = usePanelInteractions();
|
||||
@@ -67,7 +77,7 @@ function Panel({
|
||||
return (
|
||||
<div
|
||||
className={styles.panel}
|
||||
data-panel-visible={isVisible ? 'true' : 'false'}
|
||||
data-panel-visible={isOffScreen ? 'false' : 'true'}
|
||||
// Stable locator so the "Download as PNG" action can find this node to
|
||||
// capture, without threading a ref through the header/actions chain.
|
||||
data-panel-root={panelId}
|
||||
@@ -85,25 +95,23 @@ function Panel({
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
/>
|
||||
{panelDefinition && (
|
||||
<PanelBody
|
||||
panelDefinition={panelDefinition}
|
||||
panel={panel}
|
||||
panelId={panelId}
|
||||
data={data}
|
||||
isFetching={isFetching}
|
||||
isVisible={isVisible}
|
||||
isPreviousData={isPreviousData}
|
||||
error={error}
|
||||
refetch={refetch}
|
||||
onDragSelect={onDragSelect}
|
||||
dashboardPreference={dashboardPreference}
|
||||
searchTerm={searchable ? searchTerm : undefined}
|
||||
pagination={pagination}
|
||||
onClick={drilldown.onPanelClick}
|
||||
enableDrillDown={drilldown.enableDrillDown}
|
||||
/>
|
||||
)}
|
||||
<PanelBody
|
||||
panelDefinition={panelDefinition}
|
||||
panel={panel}
|
||||
panelId={panelId}
|
||||
data={data}
|
||||
isFetching={isFetching}
|
||||
isVisible={isVisible}
|
||||
isPreviousData={isPreviousData}
|
||||
error={error}
|
||||
refetch={refetch}
|
||||
onDragSelect={onDragSelect}
|
||||
dashboardPreference={dashboardPreference}
|
||||
searchTerm={searchable ? searchTerm : undefined}
|
||||
pagination={pagination}
|
||||
onClick={drilldown.onPanelClick}
|
||||
enableDrillDown={drilldown.enableDrillDown}
|
||||
/>
|
||||
<ContextMenu {...drilldown.contextMenuProps} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { type KeyboardEvent, useCallback } from 'react';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
|
||||
import styles from './ViewPanelModal.module.scss';
|
||||
|
||||
interface ViewPanelQueryBuilderProps {
|
||||
panelType: PANEL_TYPES;
|
||||
/** Preview fetch in flight — drives the Run/Cancel button state. */
|
||||
isLoadingQueries: boolean;
|
||||
/** Run the current query (Run Query button / ⌘↵). */
|
||||
onStageRunQuery: () => void;
|
||||
/** Abort the in-flight preview fetch. */
|
||||
onCancelQuery: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drilldown query editor for the View modal. Mirrors V1's FullView: the query builder
|
||||
* rows + a "Run Query" button, with NO query-type tabs (ClickHouse/PromQL) — drilldown
|
||||
* is query-builder only, exactly as V1.
|
||||
*/
|
||||
function ViewPanelQueryBuilder({
|
||||
panelType,
|
||||
isLoadingQueries,
|
||||
onStageRunQuery,
|
||||
onCancelQuery,
|
||||
}: ViewPanelQueryBuilderProps): JSX.Element {
|
||||
const handleKeyDownCapture = useCallback(
|
||||
(event: KeyboardEvent<HTMLDivElement>): void => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onStageRunQuery();
|
||||
}
|
||||
},
|
||||
[onStageRunQuery],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.queryBuilder}
|
||||
data-testid="view-panel-query-builder"
|
||||
onKeyDownCapture={handleKeyDownCapture}
|
||||
role="presentation"
|
||||
>
|
||||
<QueryBuilderV2
|
||||
panelType={panelType}
|
||||
version="v3"
|
||||
isListViewPanel={panelType === PANEL_TYPES.LIST}
|
||||
signalSourceChangeEnabled
|
||||
/>
|
||||
<div className={styles.queryBuilderToolbar}>
|
||||
<RightToolbarActions
|
||||
handleCancelQuery={onCancelQuery}
|
||||
onStageRunQuery={onStageRunQuery}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewPanelQueryBuilder;
|
||||
@@ -162,7 +162,9 @@ describe('useCreateAlertFromPanel', () => {
|
||||
expect(mockBuildQueryRangeRequest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
queries: panel.spec.queries,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
queryCapabilities: expect.objectContaining({
|
||||
requestType: 'time_series',
|
||||
}),
|
||||
variables: { service: { type: 'query', value: 'checkout' } },
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -83,6 +83,7 @@ export function useClonePanel({
|
||||
void logEvent(DashboardDetailEvents.PanelAction, {
|
||||
action: 'clone',
|
||||
panelType: PANEL_KIND_TO_PANEL_TYPE[source.panel.spec.plugin.kind],
|
||||
panelKind: source.panel.spec.plugin.kind,
|
||||
panelId,
|
||||
...eventMeta,
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import { buildQueryRangeRequest } from 'pages/DashboardPage/DashboardContainer/q
|
||||
import { envelopesToQuery } from 'pages/DashboardPage/DashboardContainer/queryV5/persesQueryAdapters';
|
||||
import { selectResolvedVariables } from 'pages/DashboardPage/DashboardContainer/store/slices/variableSelectionSlice';
|
||||
import { useDashboardStore } from 'pages/DashboardPage/DashboardContainer/store/useDashboardStore';
|
||||
import { getPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/registry';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
@@ -47,11 +48,15 @@ export function useCreateAlertFromPanel(): (
|
||||
|
||||
return useCallback(
|
||||
(panel: DashboardtypesPanelDTO, panelId: string): void => {
|
||||
const panelType = PANEL_KIND_TO_PANEL_TYPE[panel.spec.plugin.kind];
|
||||
const panelKind = panel.spec.plugin.kind;
|
||||
// Alerts are a V1 surface: the query pivots through the V1 `Query` shape and the
|
||||
// URL carries a legacy panel type, so this flow keeps translating.
|
||||
const panelType = PANEL_KIND_TO_PANEL_TYPE[panelKind];
|
||||
|
||||
void logEvent(DashboardDetailEvents.PanelAction, {
|
||||
action: 'createAlerts',
|
||||
panelType,
|
||||
panelKind,
|
||||
...eventMeta,
|
||||
widgetId: panelId,
|
||||
queryType: getPanelQueryType(panel),
|
||||
@@ -65,7 +70,7 @@ export function useCreateAlertFromPanel(): (
|
||||
// Redux global time is nanoseconds; the request DTO takes epoch ms.
|
||||
const request = buildQueryRangeRequest({
|
||||
queries: panel.spec.queries,
|
||||
panelType,
|
||||
queryCapabilities: getPanelDefinition(panelKind).queryCapabilities,
|
||||
startMs: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
|
||||
endMs: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
|
||||
variables,
|
||||
|
||||
@@ -44,6 +44,7 @@ export function useDeletePanel({
|
||||
}
|
||||
|
||||
const removed = section.items.find((i) => i.id === panelId);
|
||||
const removedKind = removed?.panel?.spec.plugin.kind;
|
||||
const nextItems = section.items.filter((i) => i.id !== panelId);
|
||||
try {
|
||||
await patchAsync([
|
||||
@@ -52,9 +53,15 @@ export function useDeletePanel({
|
||||
]);
|
||||
void logEvent(DashboardDetailEvents.PanelAction, {
|
||||
action: 'delete',
|
||||
panelType: removed?.panel
|
||||
? PANEL_KIND_TO_PANEL_TYPE[removed.panel.spec.plugin.kind]
|
||||
: undefined,
|
||||
// An item ref can outlive its panel, so both fields go on together or
|
||||
// not at all: `panelType` keeps existing reports resolving, `panelKind`
|
||||
// is the V2 identity.
|
||||
...(removedKind
|
||||
? {
|
||||
panelType: PANEL_KIND_TO_PANEL_TYPE[removedKind],
|
||||
panelKind: removedKind,
|
||||
}
|
||||
: {}),
|
||||
panelId,
|
||||
...eventMeta,
|
||||
});
|
||||
|
||||
@@ -43,6 +43,7 @@ export function useDownloadPanelCsv({
|
||||
void logEvent(DashboardDetailEvents.PanelExported, {
|
||||
format: 'csv',
|
||||
panelType: PANEL_KIND_TO_PANEL_TYPE[panel.spec.plugin.kind],
|
||||
panelKind: panel.spec.plugin.kind,
|
||||
});
|
||||
}, [canDownloadCsv, fileName, panel, data]);
|
||||
}
|
||||
|
||||
@@ -128,11 +128,14 @@ export function useDrilldown(
|
||||
|
||||
const onPanelClick = useCallback(
|
||||
(payload: DrilldownClickPayload): void => {
|
||||
void logEvent(DashboardDetailEvents.DrilldownOpened, { panelType });
|
||||
void logEvent(DashboardDetailEvents.DrilldownOpened, {
|
||||
panelType,
|
||||
panelKind: kind,
|
||||
});
|
||||
setSubMenu(DrilldownSubMenu.Base);
|
||||
onClick(payload.coordinates, payload.context);
|
||||
},
|
||||
[onClick, panelType],
|
||||
[onClick, panelType, kind],
|
||||
);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
@@ -176,7 +179,8 @@ export function useDrilldown(
|
||||
|
||||
const { resolvedQuery, isResolving } = useResolvedDrilldownQuery({
|
||||
queries,
|
||||
panelType,
|
||||
panelKind: kind,
|
||||
queryCapabilities: getPanelDefinition(kind).queryCapabilities,
|
||||
v1Query,
|
||||
enabled: showAggregateMenu,
|
||||
});
|
||||
|
||||
@@ -55,6 +55,7 @@ export function useMovePanelToSection({
|
||||
if (!moved) {
|
||||
return;
|
||||
}
|
||||
const movedKind = moved.panel?.spec.plugin.kind;
|
||||
|
||||
const sourceItems = source.items.filter((i) => i.id !== panelId);
|
||||
// Land at the section bottom, not backfilled into a gap — least disruptive
|
||||
@@ -73,9 +74,15 @@ export function useMovePanelToSection({
|
||||
);
|
||||
void logEvent(DashboardDetailEvents.PanelAction, {
|
||||
action: 'move',
|
||||
panelType: moved.panel
|
||||
? PANEL_KIND_TO_PANEL_TYPE[moved.panel.spec.plugin.kind]
|
||||
: undefined,
|
||||
// An item ref can outlive its panel, so both fields go on together or
|
||||
// not at all: `panelType` keeps existing reports resolving, `panelKind`
|
||||
// is the V2 identity.
|
||||
...(movedKind
|
||||
? {
|
||||
panelType: PANEL_KIND_TO_PANEL_TYPE[movedKind],
|
||||
panelKind: movedKind,
|
||||
}
|
||||
: {}),
|
||||
panelId,
|
||||
...eventMeta,
|
||||
});
|
||||
|
||||
@@ -3,11 +3,15 @@ import { useEffect, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useReplaceVariables } from 'api/generated/services/querier';
|
||||
import type { DashboardtypesQueryDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { buildQueryRangeRequest } from 'pages/DashboardPage/DashboardContainer/queryV5/buildQueryRangeRequest';
|
||||
import { envelopesToQuery } from 'pages/DashboardPage/DashboardContainer/queryV5/persesQueryAdapters';
|
||||
import { selectResolvedVariables } from 'pages/DashboardPage/DashboardContainer/store/slices/variableSelectionSlice';
|
||||
import { useDashboardStore } from 'pages/DashboardPage/DashboardContainer/store/useDashboardStore';
|
||||
import type { PanelQueryCapabilities } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelCapabilities';
|
||||
import {
|
||||
PANEL_KIND_TO_PANEL_TYPE,
|
||||
type PanelKind,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
@@ -15,7 +19,9 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
interface UseResolvedDrilldownQueryArgs {
|
||||
/** Panel's perses queries — the substitution source (carries the `$var` refs). */
|
||||
queries: DashboardtypesQueryDTO[];
|
||||
panelType: PANEL_TYPES;
|
||||
panelKind: PanelKind;
|
||||
/** The panel kind's declared query capabilities — shapes the substitution request. */
|
||||
queryCapabilities: PanelQueryCapabilities;
|
||||
/** The raw V5→V1 query; the fallback until substitution resolves / when no vars exist. */
|
||||
v1Query: Query;
|
||||
/** Resolve only while the aggregate menu is open (V1 parity: fires when it appears). */
|
||||
@@ -38,7 +44,8 @@ interface UseResolvedDrilldownQueryResult {
|
||||
*/
|
||||
export function useResolvedDrilldownQuery({
|
||||
queries,
|
||||
panelType,
|
||||
panelKind,
|
||||
queryCapabilities,
|
||||
v1Query,
|
||||
enabled,
|
||||
}: UseResolvedDrilldownQueryArgs): UseResolvedDrilldownQueryResult {
|
||||
@@ -60,7 +67,7 @@ export function useResolvedDrilldownQuery({
|
||||
substituteVars({
|
||||
data: buildQueryRangeRequest({
|
||||
queries,
|
||||
panelType,
|
||||
queryCapabilities,
|
||||
startMs: Math.floor(minTime / 1e6),
|
||||
endMs: Math.floor(maxTime / 1e6),
|
||||
variables,
|
||||
@@ -70,7 +77,7 @@ export function useResolvedDrilldownQuery({
|
||||
enabled,
|
||||
hasVariables,
|
||||
queries,
|
||||
panelType,
|
||||
queryCapabilities,
|
||||
minTime,
|
||||
maxTime,
|
||||
variables,
|
||||
@@ -81,8 +88,13 @@ export function useResolvedDrilldownQuery({
|
||||
if (!hasVariables || !data) {
|
||||
return v1Query;
|
||||
}
|
||||
return envelopesToQuery(data.data.compositeQuery?.queries ?? [], panelType);
|
||||
}, [hasVariables, data, v1Query, panelType]);
|
||||
// View-in-X navigates to a V1 explorer, so the resolved query crosses back into the
|
||||
// V1 `Query` shape — the one place this hook still needs a legacy panel type.
|
||||
return envelopesToQuery(
|
||||
data.data.compositeQuery?.queries ?? [],
|
||||
PANEL_KIND_TO_PANEL_TYPE[panelKind],
|
||||
);
|
||||
}, [hasVariables, data, v1Query, panelKind]);
|
||||
|
||||
return { resolvedQuery, isResolving: enabled && hasVariables && isLoading };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
type DashboardtypesPanelDTO,
|
||||
Querybuildertypesv5RequestTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { PanelQueryCapabilities } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelCapabilities';
|
||||
import {
|
||||
DASHBOARD_CACHE_TIME,
|
||||
DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
|
||||
@@ -54,6 +58,23 @@ function panelWith(
|
||||
} as unknown as DashboardtypesPanelDTO;
|
||||
}
|
||||
|
||||
// The capability blocks TimeSeries and List declare. Passed in rather than resolved from
|
||||
// the registry: the hook takes them as input, and importing the registry here would pull
|
||||
// every panel renderer (and the app's API client) into this suite.
|
||||
const TIME_SERIES_CAPABILITIES: PanelQueryCapabilities = {
|
||||
requestType: Querybuildertypesv5RequestTypeDTO.time_series,
|
||||
formatTableResultForUI: false,
|
||||
bucketedStepInterval: false,
|
||||
orderTiebreaker: false,
|
||||
serverPaginated: false,
|
||||
};
|
||||
const LIST_PANEL_CAPABILITIES: PanelQueryCapabilities = {
|
||||
...TIME_SERIES_CAPABILITIES,
|
||||
requestType: Querybuildertypesv5RequestTypeDTO.raw,
|
||||
orderTiebreaker: true,
|
||||
serverPaginated: true,
|
||||
};
|
||||
|
||||
function builderPanel(): DashboardtypesPanelDTO {
|
||||
return panelWith('signoz/TimeSeriesPanel', {
|
||||
name: 'A',
|
||||
@@ -100,7 +121,13 @@ beforeEach(() => {
|
||||
|
||||
describe('usePanelQuery', () => {
|
||||
it('builds the generated V5 request DTO directly from panel.spec.queries', () => {
|
||||
renderHook(() => usePanelQuery({ panel: builderPanel(), panelId: 'p1' }));
|
||||
renderHook(() =>
|
||||
usePanelQuery({
|
||||
panel: builderPanel(),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
const [{ requestPayload }] = mockUseGetQueryRangeV5.mock.calls[0];
|
||||
expect(requestPayload.schemaVersion).toBe('v1');
|
||||
expect(requestPayload.compositeQuery.queries).toStrictEqual([
|
||||
@@ -112,30 +139,30 @@ describe('usePanelQuery', () => {
|
||||
});
|
||||
|
||||
it('converts redux nanosecond time to epoch ms on the request', () => {
|
||||
renderHook(() => usePanelQuery({ panel: builderPanel(), panelId: 'p1' }));
|
||||
renderHook(() =>
|
||||
usePanelQuery({
|
||||
panel: builderPanel(),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
const [{ requestPayload }] = mockUseGetQueryRangeV5.mock.calls[0];
|
||||
expect(requestPayload.start).toBe(1_000_000_000);
|
||||
expect(requestPayload.end).toBe(2_000_000_000);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['signoz/TimeSeriesPanel', 'time_series'],
|
||||
['signoz/ListPanel', 'raw'],
|
||||
// HISTOGRAM and BAR panels bin/derive from raw time-series data
|
||||
// client-side, so the backend must receive `time_series` (V1 parity).
|
||||
['signoz/HistogramPanel', 'time_series'],
|
||||
['signoz/BarChartPanel', 'time_series'],
|
||||
['signoz/NumberPanel', 'scalar'],
|
||||
['signoz/PieChartPanel', 'scalar'],
|
||||
])('%s panel sends requestType=%s', (panelKind, requestType) => {
|
||||
// Which requestType each kind declares is asserted in
|
||||
// Panels/__tests__/capabilities.test.ts; here it only has to reach the request.
|
||||
it('sends the requestType from the declared query capabilities', () => {
|
||||
renderHook(() =>
|
||||
usePanelQuery({
|
||||
panel: panelWith(panelKind, { name: 'A', signal: 'logs' }),
|
||||
panel: panelWith('signoz/ListPanel', { name: 'A', signal: 'logs' }),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
const [{ requestPayload }] = mockUseGetQueryRangeV5.mock.calls[0];
|
||||
expect(requestPayload.requestType).toBe(requestType);
|
||||
expect(requestPayload.requestType).toBe('raw');
|
||||
});
|
||||
|
||||
it('exposes the raw V5 response, request payload, and legend map on data', () => {
|
||||
@@ -148,7 +175,11 @@ describe('usePanelQuery', () => {
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelQuery({ panel: builderPanel(), panelId: 'p1' }),
|
||||
usePanelQuery({
|
||||
panel: builderPanel(),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.data.response).toBe(v5Response);
|
||||
@@ -158,7 +189,11 @@ describe('usePanelQuery', () => {
|
||||
|
||||
it('exposes an undefined response before data arrives', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelQuery({ panel: builderPanel(), panelId: 'p1' }),
|
||||
usePanelQuery({
|
||||
panel: builderPanel(),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
expect(result.current.data.response).toBeUndefined();
|
||||
});
|
||||
@@ -171,7 +206,11 @@ describe('usePanelQuery', () => {
|
||||
error: new Error('boom'),
|
||||
});
|
||||
const { result } = renderHook(() =>
|
||||
usePanelQuery({ panel: builderPanel(), panelId: 'p1' }),
|
||||
usePanelQuery({
|
||||
panel: builderPanel(),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
expect(result.current.error?.message).toBe('boom');
|
||||
});
|
||||
@@ -186,7 +225,11 @@ describe('usePanelQuery', () => {
|
||||
error: null,
|
||||
});
|
||||
const { result } = renderHook(() =>
|
||||
usePanelQuery({ panel: builderPanel(), panelId: 'p1' }),
|
||||
usePanelQuery({
|
||||
panel: builderPanel(),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.isFetching).toBe(true);
|
||||
@@ -200,7 +243,11 @@ describe('usePanelQuery', () => {
|
||||
error: null,
|
||||
});
|
||||
const { result } = renderHook(() =>
|
||||
usePanelQuery({ panel: builderPanel(), panelId: 'p1' }),
|
||||
usePanelQuery({
|
||||
panel: builderPanel(),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
});
|
||||
@@ -213,14 +260,23 @@ describe('usePanelQuery', () => {
|
||||
error: undefined,
|
||||
});
|
||||
const { result } = renderHook(() =>
|
||||
usePanelQuery({ panel: builderPanel(), panelId: 'p1' }),
|
||||
usePanelQuery({
|
||||
panel: builderPanel(),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
it('passes enabled=false to the fetch hook when the caller disables it', () => {
|
||||
renderHook(() =>
|
||||
usePanelQuery({ panel: builderPanel(), panelId: 'p1', enabled: false }),
|
||||
usePanelQuery({
|
||||
panel: builderPanel(),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
enabled: false,
|
||||
}),
|
||||
);
|
||||
const [{ enabled }] = mockUseGetQueryRangeV5.mock.calls[0];
|
||||
expect(enabled).toBe(false);
|
||||
@@ -228,7 +284,12 @@ describe('usePanelQuery', () => {
|
||||
|
||||
it('auto-disables the fetch when the panel has no queries (even with enabled=true)', () => {
|
||||
renderHook(() =>
|
||||
usePanelQuery({ panel: emptyPanel(), panelId: 'p1', enabled: true }),
|
||||
usePanelQuery({
|
||||
panel: emptyPanel(),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
enabled: true,
|
||||
}),
|
||||
);
|
||||
const [{ enabled }] = mockUseGetQueryRangeV5.mock.calls[0];
|
||||
expect(enabled).toBe(false);
|
||||
@@ -243,6 +304,7 @@ describe('usePanelQuery', () => {
|
||||
aggregations: [{}],
|
||||
}),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
const [{ enabled }] = mockUseGetQueryRangeV5.mock.calls[0];
|
||||
@@ -251,7 +313,13 @@ describe('usePanelQuery', () => {
|
||||
|
||||
it('composes a react-query cache key that includes panelId, time range, kind, and queries', () => {
|
||||
const panel = builderPanel();
|
||||
renderHook(() => usePanelQuery({ panel, panelId: 'p1' }));
|
||||
renderHook(() =>
|
||||
usePanelQuery({
|
||||
panel,
|
||||
panelId: 'p1',
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
const [{ queryKey }] = mockUseGetQueryRangeV5.mock.calls[0];
|
||||
expect(queryKey).toStrictEqual(
|
||||
expect.arrayContaining([
|
||||
@@ -270,6 +338,7 @@ describe('usePanelQuery', () => {
|
||||
renderHook(() =>
|
||||
usePanelQuery({
|
||||
panel,
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
panelId: 'p1',
|
||||
time: { startMs: 1_700_000_000_000, endMs: 1_700_000_600_000 },
|
||||
}),
|
||||
@@ -296,6 +365,7 @@ describe('usePanelQuery', () => {
|
||||
usePanelQuery({
|
||||
panel: builderPanel(),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
time: { startMs: 1_700_000_000_000.546, endMs: 1_700_000_600_000.999 },
|
||||
}),
|
||||
);
|
||||
@@ -316,7 +386,11 @@ describe('usePanelQuery', () => {
|
||||
|
||||
it('exposes server paging at the default page size when the query has no limit', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelQuery({ panel: listPanel({}), panelId: 'p1' }),
|
||||
usePanelQuery({
|
||||
panel: listPanel({}),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
expect(result.current.pagination).toBeDefined();
|
||||
expect(result.current.pagination?.pageSize).toBe(25);
|
||||
@@ -327,20 +401,34 @@ describe('usePanelQuery', () => {
|
||||
|
||||
it('disables the server pager when the query has an explicit limit (V1 parity)', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelQuery({ panel: listPanel({ limit: 100 }), panelId: 'p1' }),
|
||||
usePanelQuery({
|
||||
panel: listPanel({ limit: 100 }),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
expect(result.current.pagination).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps previous data while paging so the table/pager stay mounted on page change', () => {
|
||||
renderHook(() => usePanelQuery({ panel: listPanel({}), panelId: 'p1' }));
|
||||
renderHook(() =>
|
||||
usePanelQuery({
|
||||
panel: listPanel({}),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
const [{ keepPreviousData }] = mockUseGetQueryRangeV5.mock.calls[0];
|
||||
expect(keepPreviousData).toBe(true);
|
||||
});
|
||||
|
||||
it('changes the page size (and re-requests with the new limit) via setPageSize', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelQuery({ panel: listPanel({}), panelId: 'p1' }),
|
||||
usePanelQuery({
|
||||
panel: listPanel({}),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
|
||||
act(() => result.current.pagination?.setPageSize(50));
|
||||
@@ -380,7 +468,11 @@ describe('usePanelQuery', () => {
|
||||
|
||||
it('starts on page 0 with no prev/next and does not throw before data arrives', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelQuery({ panel: listPanel({}), panelId: 'p1' }),
|
||||
usePanelQuery({
|
||||
panel: listPanel({}),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
expect(result.current.pagination?.pageIndex).toBe(0);
|
||||
expect(result.current.pagination?.canPrev).toBe(false);
|
||||
@@ -392,21 +484,33 @@ describe('usePanelQuery', () => {
|
||||
// window/cursor path), so a full page is the has-more signal.
|
||||
withResponse(rawResponse(25));
|
||||
const fullPage = renderHook(() =>
|
||||
usePanelQuery({ panel: listPanel({}), panelId: 'p1' }),
|
||||
usePanelQuery({
|
||||
panel: listPanel({}),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
expect(fullPage.result.current.pagination?.canNext).toBe(true);
|
||||
|
||||
// Partial page, no cursor → the last page.
|
||||
withResponse(rawResponse(3));
|
||||
const partialPage = renderHook(() =>
|
||||
usePanelQuery({ panel: listPanel({}), panelId: 'p1' }),
|
||||
usePanelQuery({
|
||||
panel: listPanel({}),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
expect(partialPage.result.current.pagination?.canNext).toBe(false);
|
||||
|
||||
// Cursor present (even on a partial page) → more rows (timestamp window path).
|
||||
withResponse(rawResponse(3, 'cursor-1'));
|
||||
const withCursor = renderHook(() =>
|
||||
usePanelQuery({ panel: listPanel({}), panelId: 'p1' }),
|
||||
usePanelQuery({
|
||||
panel: listPanel({}),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
expect(withCursor.result.current.pagination?.canNext).toBe(true);
|
||||
});
|
||||
@@ -416,7 +520,13 @@ describe('usePanelQuery', () => {
|
||||
// Stable panel reference: a fresh one each render would change the
|
||||
// `queries` identity and trip the offset-reset effect (real props are stable).
|
||||
const panel = listPanel({});
|
||||
const { result } = renderHook(() => usePanelQuery({ panel, panelId: 'p1' }));
|
||||
const { result } = renderHook(() =>
|
||||
usePanelQuery({
|
||||
panel,
|
||||
panelId: 'p1',
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
expect(result.current.pagination?.pageIndex).toBe(0);
|
||||
|
||||
act(() => result.current.pagination?.goNext());
|
||||
@@ -428,7 +538,11 @@ describe('usePanelQuery', () => {
|
||||
it('stays defined and zero-paged for a non-raw (scalar) response', () => {
|
||||
withResponse({ data: { type: 'scalar', data: { results: [] } } });
|
||||
const { result } = renderHook(() =>
|
||||
usePanelQuery({ panel: listPanel({}), panelId: 'p1' }),
|
||||
usePanelQuery({
|
||||
panel: listPanel({}),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
expect(result.current.pagination).toBeDefined();
|
||||
expect(result.current.pagination?.canNext).toBe(false);
|
||||
@@ -437,7 +551,11 @@ describe('usePanelQuery', () => {
|
||||
|
||||
it('ignores a non-positive page size so paging never goes invalid', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelQuery({ panel: listPanel({}), panelId: 'p1' }),
|
||||
usePanelQuery({
|
||||
panel: listPanel({}),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
act(() => result.current.pagination?.setPageSize(0));
|
||||
expect(result.current.pagination?.pageSize).toBe(25);
|
||||
@@ -456,14 +574,26 @@ describe('usePanelQuery', () => {
|
||||
|
||||
it('caches for DASHBOARD_CACHE_TIME when auto-refresh is disabled', () => {
|
||||
withAutoRefreshDisabled(true);
|
||||
renderHook(() => usePanelQuery({ panel: builderPanel(), panelId: 'p1' }));
|
||||
renderHook(() =>
|
||||
usePanelQuery({
|
||||
panel: builderPanel(),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
const [{ cacheTime }] = mockUseGetQueryRangeV5.mock.calls[0];
|
||||
expect(cacheTime).toBe(DASHBOARD_CACHE_TIME);
|
||||
});
|
||||
|
||||
it('drops cacheTime to 0 when auto-refresh is enabled', () => {
|
||||
withAutoRefreshDisabled(false);
|
||||
renderHook(() => usePanelQuery({ panel: builderPanel(), panelId: 'p1' }));
|
||||
renderHook(() =>
|
||||
usePanelQuery({
|
||||
panel: builderPanel(),
|
||||
panelId: 'p1',
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
}),
|
||||
);
|
||||
const [{ cacheTime }] = mockUseGetQueryRangeV5.mock.calls[0];
|
||||
expect(cacheTime).toBe(DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useQueryClient } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports -- TODO: migrate global time selector off redux
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
DASHBOARD_CACHE_TIME,
|
||||
DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
|
||||
@@ -24,7 +23,7 @@ import {
|
||||
queryReferencesAnyVariable,
|
||||
} from '../queryV5/getReferencedVariables';
|
||||
import { getBuilderQueries } from '../Panels/utils/getBuilderQueries';
|
||||
import { PANEL_KIND_TO_PANEL_TYPE } from '../Panels/types/panelKind';
|
||||
import type { PanelQueryCapabilities } from '../Panels/types/panelCapabilities';
|
||||
import { selectResolvedVariables } from '../store/slices/variableSelectionSlice';
|
||||
import { useDashboardStore } from '../store/useDashboardStore';
|
||||
import { resolvePanelTimeWindow } from './resolvePanelTimeWindow';
|
||||
@@ -38,6 +37,8 @@ const DEFAULT_LIST_PAGE_SIZE = 25;
|
||||
export interface UsePanelQueryArgs {
|
||||
panel: DashboardtypesPanelDTO;
|
||||
panelId: string;
|
||||
/** The panel kind's declared query capabilities — `panelDefinition.queryCapabilities` at the call site. */
|
||||
queryCapabilities: PanelQueryCapabilities;
|
||||
/**
|
||||
* Gate the fetch (default true). PanelV2 sets false for unregistered kinds to skip a wasted
|
||||
* call. The hook also auto-disables internally when the panel has no runnable queries.
|
||||
@@ -85,21 +86,20 @@ export interface UsePanelQueryResult {
|
||||
export function usePanelQuery({
|
||||
panel,
|
||||
panelId,
|
||||
queryCapabilities,
|
||||
enabled = true,
|
||||
time,
|
||||
}: UsePanelQueryArgs): UsePanelQueryResult {
|
||||
const fullKind = panel.spec.plugin.kind;
|
||||
const panelType =
|
||||
(fullKind && PANEL_KIND_TO_PANEL_TYPE[fullKind]) ?? PANEL_TYPES.TIME_SERIES;
|
||||
const queries = panel.spec.queries;
|
||||
|
||||
// V1 parity: a list query with an explicit `limit` shows without a server pager; without
|
||||
// one it pages server-side at a user-selectable size.
|
||||
// V1 parity: a query with an explicit `limit` shows without a server pager; without
|
||||
// one a paging kind fetches server-side at a user-selectable size.
|
||||
const hasExplicitLimit = useMemo(
|
||||
() => !!getBuilderQueries(queries)[0]?.limit,
|
||||
[queries],
|
||||
);
|
||||
const isPaginated = panelType === PANEL_TYPES.LIST && !hasExplicitLimit;
|
||||
const isPaginated = queryCapabilities.serverPaginated && !hasExplicitLimit;
|
||||
|
||||
const [pageSize, setPageSize] = useState(DEFAULT_LIST_PAGE_SIZE);
|
||||
const [offset, setOffset] = useState(0);
|
||||
@@ -188,7 +188,7 @@ export function usePanelQuery({
|
||||
() =>
|
||||
buildQueryRangeRequest({
|
||||
queries,
|
||||
panelType,
|
||||
queryCapabilities,
|
||||
startMs,
|
||||
endMs,
|
||||
fillGaps,
|
||||
@@ -197,7 +197,7 @@ export function usePanelQuery({
|
||||
}),
|
||||
[
|
||||
queries,
|
||||
panelType,
|
||||
queryCapabilities,
|
||||
startMs,
|
||||
endMs,
|
||||
fillGaps,
|
||||
|
||||
@@ -1,62 +1,37 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
DashboardtypesDynamicVariableSignalDTO,
|
||||
type DashboardtypesGettableDashboardV2DTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { setDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||
import type {
|
||||
IDashboardVariable,
|
||||
TVariableQueryType,
|
||||
} from 'types/api/dashboard/variables';
|
||||
type DynamicVariableSuggestion,
|
||||
setDynamicVariableSuggestions,
|
||||
} from 'providers/Dashboard/store/dynamicVariableSuggestions';
|
||||
|
||||
import { dtoToFormModel } from '../DashboardSettings/Variables/variableAdapters';
|
||||
import {
|
||||
type VariableFormModel,
|
||||
type VariableType,
|
||||
} from '../DashboardSettings/Variables/variableFormModel';
|
||||
|
||||
const TYPE_TO_V1: Record<VariableType, TVariableQueryType> = {
|
||||
QUERY: 'QUERY',
|
||||
CUSTOM: 'CUSTOM',
|
||||
TEXT: 'TEXTBOX',
|
||||
DYNAMIC: 'DYNAMIC',
|
||||
};
|
||||
|
||||
/** Minimal V1-shaped variable — only the fields the shared query builder reads. */
|
||||
function toV1Variable(model: VariableFormModel): IDashboardVariable {
|
||||
return {
|
||||
id: model.name,
|
||||
name: model.name,
|
||||
description: model.description,
|
||||
type: TYPE_TO_V1[model.type],
|
||||
queryValue: model.queryValue,
|
||||
customValue: model.customValue,
|
||||
textboxValue: model.textValue,
|
||||
sort: 'DISABLED',
|
||||
multiSelect: model.multiSelect,
|
||||
showALLOption: model.showAllOption,
|
||||
dynamicVariablesAttribute: model.dynamicAttribute,
|
||||
dynamicVariablesSource:
|
||||
model.dynamicSignal === DashboardtypesDynamicVariableSignalDTO.all
|
||||
? 'all sources'
|
||||
: model.dynamicSignal,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes the V2 dashboard's variables into the shared `dashboardVariablesStore`
|
||||
* that the query builder's autocomplete (`QuerySearch`) reads, so `$variable`
|
||||
* suggestions show up in the panel editor and the dashboards-page query builder.
|
||||
* Suggestion-only — the runtime engine lives in the V2 store. Clears on unmount so
|
||||
* the shared store doesn't leak into other pages.
|
||||
* Publishes the dashboard's dynamic variables into the shared suggestion store that
|
||||
* the query builder's autocomplete (`QuerySearch`) reads, so `$variable` is offered
|
||||
* as a value for the attribute each one backs — in the panel editor and the
|
||||
* dashboards-page query builder. Suggestion-only: the runtime engine lives in the
|
||||
* dashboard store. Clears on unmount so the shared store doesn't leak into other
|
||||
* pages.
|
||||
*/
|
||||
export function useSyncVariablesForSuggestions(
|
||||
dashboard: DashboardtypesGettableDashboardV2DTO | undefined,
|
||||
): void {
|
||||
const dashboardId = dashboard?.id ?? '';
|
||||
const specVariables = dashboard?.spec?.variables;
|
||||
const variables = useMemo(
|
||||
() => (specVariables ?? []).map(dtoToFormModel),
|
||||
const suggestions = useMemo<DynamicVariableSuggestion[]>(
|
||||
() =>
|
||||
(specVariables ?? [])
|
||||
.map(dtoToFormModel)
|
||||
.filter(
|
||||
(model) =>
|
||||
model.type === 'DYNAMIC' && !!model.name && !!model.dynamicAttribute,
|
||||
)
|
||||
.map((model) => ({
|
||||
name: model.name,
|
||||
attribute: model.dynamicAttribute,
|
||||
})),
|
||||
[specVariables],
|
||||
);
|
||||
|
||||
@@ -64,14 +39,7 @@ export function useSyncVariablesForSuggestions(
|
||||
if (!dashboardId) {
|
||||
return undefined;
|
||||
}
|
||||
const record: Record<string, IDashboardVariable> = {};
|
||||
variables.forEach((model) => {
|
||||
if (model.name) {
|
||||
record[model.name] = toV1Variable(model);
|
||||
}
|
||||
});
|
||||
setDashboardVariablesStore({ dashboardId, variables: record });
|
||||
return (): void =>
|
||||
setDashboardVariablesStore({ dashboardId: '', variables: {} });
|
||||
}, [dashboardId, variables]);
|
||||
setDynamicVariableSuggestions(suggestions);
|
||||
return (): void => setDynamicVariableSuggestions([]);
|
||||
}, [dashboardId, suggestions]);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { DashboardtypesQueryDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
type DashboardtypesQueryDTO,
|
||||
Querybuildertypesv5RequestTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import {
|
||||
buildQueryRangeRequest,
|
||||
extractLegendMap,
|
||||
getBarStepIntervalSeconds,
|
||||
hasRunnableQueries,
|
||||
panelTypeToRequestType,
|
||||
toQueryEnvelopes,
|
||||
} from '../buildQueryRangeRequest';
|
||||
|
||||
@@ -40,20 +41,46 @@ function compositeQuery(
|
||||
const HOUR_MS = 60 * 60 * 1000;
|
||||
const START_MS = 1_700_000_000_000;
|
||||
|
||||
describe('panelTypeToRequestType', () => {
|
||||
// Capability blocks matching what each kind declares, so these tests exercise the
|
||||
// builder's response to the flags rather than the declarations themselves (those are
|
||||
// asserted against the registry in Panels/__tests__/capabilities.test.ts).
|
||||
const TIME_SERIES_CAPABILITIES = {
|
||||
requestType: Querybuildertypesv5RequestTypeDTO.time_series,
|
||||
formatTableResultForUI: false,
|
||||
bucketedStepInterval: false,
|
||||
orderTiebreaker: false,
|
||||
serverPaginated: false,
|
||||
};
|
||||
const BAR_CAPABILITIES = {
|
||||
...TIME_SERIES_CAPABILITIES,
|
||||
bucketedStepInterval: true,
|
||||
};
|
||||
const TABLE_CAPABILITIES = {
|
||||
...TIME_SERIES_CAPABILITIES,
|
||||
requestType: Querybuildertypesv5RequestTypeDTO.scalar,
|
||||
formatTableResultForUI: true,
|
||||
};
|
||||
const LIST_PANEL_CAPABILITIES = {
|
||||
...TIME_SERIES_CAPABILITIES,
|
||||
requestType: Querybuildertypesv5RequestTypeDTO.raw,
|
||||
orderTiebreaker: true,
|
||||
serverPaginated: true,
|
||||
};
|
||||
|
||||
describe('requestType', () => {
|
||||
it.each([
|
||||
[PANEL_TYPES.TIME_SERIES, 'time_series'],
|
||||
// HISTOGRAM and BAR bin client-side from time-series data; sending
|
||||
// 'distribution' would return a shape the renderers can't bin.
|
||||
[PANEL_TYPES.BAR, 'time_series'],
|
||||
[PANEL_TYPES.HISTOGRAM, 'time_series'],
|
||||
[PANEL_TYPES.TABLE, 'scalar'],
|
||||
[PANEL_TYPES.PIE, 'scalar'],
|
||||
[PANEL_TYPES.VALUE, 'scalar'],
|
||||
[PANEL_TYPES.LIST, 'raw'],
|
||||
[PANEL_TYPES.TRACE, 'trace'],
|
||||
])('%s → %s', (panelType, requestType) => {
|
||||
expect(panelTypeToRequestType(panelType)).toBe(requestType);
|
||||
Querybuildertypesv5RequestTypeDTO.time_series,
|
||||
Querybuildertypesv5RequestTypeDTO.scalar,
|
||||
Querybuildertypesv5RequestTypeDTO.raw,
|
||||
Querybuildertypesv5RequestTypeDTO.trace,
|
||||
])('passes %s through from the declared capabilities', (requestType) => {
|
||||
const request = buildQueryRangeRequest({
|
||||
queries: bareBuilderQuery({ name: 'A', signal: 'metrics' }),
|
||||
queryCapabilities: { ...TIME_SERIES_CAPABILITIES, requestType },
|
||||
startMs: START_MS,
|
||||
endMs: START_MS + HOUR_MS,
|
||||
});
|
||||
expect(request.requestType).toBe(requestType);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -135,7 +162,7 @@ describe('buildQueryRangeRequest', () => {
|
||||
it('assembles the full request DTO', () => {
|
||||
const request = buildQueryRangeRequest({
|
||||
queries: bareBuilderQuery({ name: 'A', signal: 'metrics' }),
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
startMs: START_MS,
|
||||
endMs: START_MS + HOUR_MS,
|
||||
});
|
||||
@@ -157,7 +184,7 @@ describe('buildQueryRangeRequest', () => {
|
||||
it('sets formatTableResultForUI only for TABLE panels', () => {
|
||||
const request = buildQueryRangeRequest({
|
||||
queries: bareBuilderQuery({ name: 'A' }),
|
||||
panelType: PANEL_TYPES.TABLE,
|
||||
queryCapabilities: TABLE_CAPABILITIES,
|
||||
startMs: START_MS,
|
||||
endMs: START_MS + HOUR_MS,
|
||||
});
|
||||
@@ -167,7 +194,7 @@ describe('buildQueryRangeRequest', () => {
|
||||
it('passes through fillGaps into formatOptions', () => {
|
||||
const request = buildQueryRangeRequest({
|
||||
queries: bareBuilderQuery({ name: 'A' }),
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
startMs: START_MS,
|
||||
endMs: START_MS + HOUR_MS,
|
||||
fillGaps: true,
|
||||
@@ -178,7 +205,7 @@ describe('buildQueryRangeRequest', () => {
|
||||
it('stamps offset/limit onto builder queries when pagination is given', () => {
|
||||
const request = buildQueryRangeRequest({
|
||||
queries: bareBuilderQuery({ name: 'A', signal: 'logs' }),
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
startMs: START_MS,
|
||||
endMs: START_MS + HOUR_MS,
|
||||
pagination: { offset: 100, limit: 50 },
|
||||
@@ -198,7 +225,7 @@ describe('buildQueryRangeRequest', () => {
|
||||
it('defaults a logs list with no order to timestamp desc + id tiebreaker', () => {
|
||||
const request = buildQueryRangeRequest({
|
||||
queries: bareBuilderQuery({ name: 'A', signal: 'logs' }),
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
startMs: START_MS,
|
||||
endMs: START_MS + HOUR_MS,
|
||||
});
|
||||
@@ -218,7 +245,7 @@ describe('buildQueryRangeRequest', () => {
|
||||
signal: 'logs',
|
||||
order: [{ key: { name: 'timestamp' }, direction: 'desc' }],
|
||||
}),
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
startMs: START_MS,
|
||||
endMs: START_MS + HOUR_MS,
|
||||
});
|
||||
@@ -238,7 +265,7 @@ describe('buildQueryRangeRequest', () => {
|
||||
];
|
||||
const request = buildQueryRangeRequest({
|
||||
queries: bareBuilderQuery({ name: 'A', signal: 'logs', order }),
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
startMs: START_MS,
|
||||
endMs: START_MS + HOUR_MS,
|
||||
});
|
||||
@@ -252,7 +279,7 @@ describe('buildQueryRangeRequest', () => {
|
||||
const order = [{ key: { name: 'timestamp' }, direction: 'desc' }];
|
||||
const request = buildQueryRangeRequest({
|
||||
queries: bareBuilderQuery({ name: 'A', signal: 'traces', order }),
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
queryCapabilities: LIST_PANEL_CAPABILITIES,
|
||||
startMs: START_MS,
|
||||
endMs: START_MS + HOUR_MS,
|
||||
});
|
||||
@@ -265,7 +292,7 @@ describe('buildQueryRangeRequest', () => {
|
||||
it('injects the range-derived stepInterval into BAR builder queries without one', () => {
|
||||
const request = buildQueryRangeRequest({
|
||||
queries: bareBuilderQuery({ name: 'A', signal: 'metrics' }),
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
queryCapabilities: BAR_CAPABILITIES,
|
||||
startMs: START_MS,
|
||||
endMs: START_MS + HOUR_MS,
|
||||
});
|
||||
@@ -280,7 +307,7 @@ describe('buildQueryRangeRequest', () => {
|
||||
it('preserves a user-set stepInterval on BAR builder queries', () => {
|
||||
const request = buildQueryRangeRequest({
|
||||
queries: bareBuilderQuery({ name: 'A', stepInterval: 300 }),
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
queryCapabilities: BAR_CAPABILITIES,
|
||||
startMs: START_MS,
|
||||
endMs: START_MS + HOUR_MS,
|
||||
});
|
||||
@@ -293,7 +320,7 @@ describe('buildQueryRangeRequest', () => {
|
||||
it('does not touch stepInterval for non-BAR panels', () => {
|
||||
const request = buildQueryRangeRequest({
|
||||
queries: bareBuilderQuery({ name: 'A' }),
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
queryCapabilities: TIME_SERIES_CAPABILITIES,
|
||||
startMs: START_MS,
|
||||
endMs: START_MS + HOUR_MS,
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user