mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-08 18:10:27 +01:00
Compare commits
64 Commits
nv/dashboa
...
infraM/v2_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95a055ff0b | ||
|
|
8e64a1f0bc | ||
|
|
d881600bdb | ||
|
|
5a964b1a0b | ||
|
|
d547b84e06 | ||
|
|
05aea48571 | ||
|
|
a669f58430 | ||
|
|
bf226738a1 | ||
|
|
aa9a01a5b8 | ||
|
|
7fef18de2b | ||
|
|
40d76e114d | ||
|
|
b2dcbcf53e | ||
|
|
cfa1d52cfe | ||
|
|
1f6fc619e9 | ||
|
|
4873c68e7c | ||
|
|
3b73902def | ||
|
|
ab7a6959bd | ||
|
|
7e2816400e | ||
|
|
9ee9db951d | ||
|
|
3864e2a7bf | ||
|
|
72349bfd36 | ||
|
|
8b9e7827e6 | ||
|
|
f6cdf99784 | ||
|
|
9a18e6d849 | ||
|
|
568446b0a5 | ||
|
|
3497b87d66 | ||
|
|
ca0924be3e | ||
|
|
9375253ad4 | ||
|
|
7633f73805 | ||
|
|
09102a2bb0 | ||
|
|
d71c050250 | ||
|
|
50f01ac0da | ||
|
|
9bff1d00a2 | ||
|
|
6be4031c15 | ||
|
|
a38b7f758c | ||
|
|
54f925203d | ||
|
|
68c3f6b6e4 | ||
|
|
25576e8cbe | ||
|
|
cad1e2dbb8 | ||
|
|
c9c9b509b1 | ||
|
|
308b0b0fab | ||
|
|
63dd524ca1 | ||
|
|
7f0cd8f5cd | ||
|
|
7cab3e0c8f | ||
|
|
5cadf8582e | ||
|
|
b23ba1c960 | ||
|
|
4ec6b760c8 | ||
|
|
09d7c8b909 | ||
|
|
abcdb32c2e | ||
|
|
446355ebfb | ||
|
|
0389550757 | ||
|
|
2f60e6e75c | ||
|
|
896d77e3ec | ||
|
|
981aab436f | ||
|
|
711bbf49f1 | ||
|
|
1cc48405e0 | ||
|
|
e79715bf6d | ||
|
|
2ce86a3f9d | ||
|
|
639e3239a9 | ||
|
|
e83a23e6ab | ||
|
|
f0cff4ae96 | ||
|
|
0667dc47cb | ||
|
|
4af267ee61 | ||
|
|
6f1090818c |
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -43,6 +43,7 @@ jobs:
|
||||
- cloudintegrations
|
||||
- dashboard
|
||||
- ingestionkeys
|
||||
- inframonitoring
|
||||
- logspipelines
|
||||
- passwordauthn
|
||||
- preference
|
||||
|
||||
@@ -190,7 +190,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.127.1
|
||||
image: signoz/signoz:v0.127.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.127.1
|
||||
image: signoz/signoz:v0.127.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
volumes:
|
||||
|
||||
@@ -181,7 +181,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.127.1}
|
||||
image: signoz/signoz:${VERSION:-v0.127.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -109,7 +109,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.127.1}
|
||||
image: signoz/signoz:${VERSION:-v0.127.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -96,19 +96,6 @@ components:
|
||||
- createdAt
|
||||
- updatedAt
|
||||
type: object
|
||||
AlertmanagertypesGoogleChatReceiverConfig:
|
||||
properties:
|
||||
http_config:
|
||||
$ref: '#/components/schemas/ConfigHTTPClientConfig'
|
||||
send_resolved:
|
||||
type: boolean
|
||||
text:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
webhook_url:
|
||||
$ref: '#/components/schemas/ConfigSecretURL'
|
||||
type: object
|
||||
AlertmanagertypesMaintenanceKind:
|
||||
enum:
|
||||
- fixed
|
||||
@@ -160,8 +147,6 @@ components:
|
||||
type: object
|
||||
AlertmanagertypesPostableChannel:
|
||||
oneOf:
|
||||
- required:
|
||||
- googlechat_configs
|
||||
- required:
|
||||
- discord_configs
|
||||
- required:
|
||||
@@ -207,10 +192,6 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigEmailConfig'
|
||||
type: array
|
||||
googlechat_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/AlertmanagertypesGoogleChatReceiverConfig'
|
||||
type: array
|
||||
incidentio_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigIncidentioConfig'
|
||||
@@ -324,87 +305,6 @@ components:
|
||||
- channels
|
||||
- name
|
||||
type: object
|
||||
AlertmanagertypesReceiver:
|
||||
properties:
|
||||
discord_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigDiscordConfig'
|
||||
type: array
|
||||
email_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigEmailConfig'
|
||||
type: array
|
||||
googlechat_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/AlertmanagertypesGoogleChatReceiverConfig'
|
||||
type: array
|
||||
incidentio_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigIncidentioConfig'
|
||||
type: array
|
||||
jira_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigJiraConfig'
|
||||
type: array
|
||||
mattermost_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMattermostConfig'
|
||||
type: array
|
||||
msteams_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMSTeamsConfig'
|
||||
type: array
|
||||
msteamsv2_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMSTeamsV2Config'
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
opsgenie_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigOpsGenieConfig'
|
||||
type: array
|
||||
pagerduty_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigPagerdutyConfig'
|
||||
type: array
|
||||
pushover_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigPushoverConfig'
|
||||
type: array
|
||||
rocketchat_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigRocketchatConfig'
|
||||
type: array
|
||||
slack_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigSlackConfig'
|
||||
type: array
|
||||
sns_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigSNSConfig'
|
||||
type: array
|
||||
telegram_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigTelegramConfig'
|
||||
type: array
|
||||
victorops_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigVictorOpsConfig'
|
||||
type: array
|
||||
webex_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWebexConfig'
|
||||
type: array
|
||||
webhook_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWebhookConfig'
|
||||
type: array
|
||||
wechat_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWechatConfig'
|
||||
type: array
|
||||
type: object
|
||||
AlertmanagertypesRecurrence:
|
||||
properties:
|
||||
duration:
|
||||
@@ -1965,6 +1865,83 @@ components:
|
||||
user_key_file:
|
||||
type: string
|
||||
type: object
|
||||
ConfigReceiver:
|
||||
properties:
|
||||
discord_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigDiscordConfig'
|
||||
type: array
|
||||
email_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigEmailConfig'
|
||||
type: array
|
||||
incidentio_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigIncidentioConfig'
|
||||
type: array
|
||||
jira_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigJiraConfig'
|
||||
type: array
|
||||
mattermost_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMattermostConfig'
|
||||
type: array
|
||||
msteams_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMSTeamsConfig'
|
||||
type: array
|
||||
msteamsv2_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigMSTeamsV2Config'
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
opsgenie_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigOpsGenieConfig'
|
||||
type: array
|
||||
pagerduty_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigPagerdutyConfig'
|
||||
type: array
|
||||
pushover_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigPushoverConfig'
|
||||
type: array
|
||||
rocketchat_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigRocketchatConfig'
|
||||
type: array
|
||||
slack_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigSlackConfig'
|
||||
type: array
|
||||
sns_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigSNSConfig'
|
||||
type: array
|
||||
telegram_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigTelegramConfig'
|
||||
type: array
|
||||
victorops_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigVictorOpsConfig'
|
||||
type: array
|
||||
webex_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWebexConfig'
|
||||
type: array
|
||||
webhook_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWebhookConfig'
|
||||
type: array
|
||||
wechat_configs:
|
||||
items:
|
||||
$ref: '#/components/schemas/ConfigWechatConfig'
|
||||
type: array
|
||||
type: object
|
||||
ConfigRocketchatAttachmentAction:
|
||||
properties:
|
||||
image_url:
|
||||
@@ -2419,19 +2396,6 @@ components:
|
||||
repeatVariable:
|
||||
type: string
|
||||
type: object
|
||||
DashboardLink:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
renderVariables:
|
||||
type: boolean
|
||||
targetBlank:
|
||||
type: boolean
|
||||
tooltip:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
DashboardTextVariableSpec:
|
||||
properties:
|
||||
constant:
|
||||
@@ -2552,20 +2516,22 @@ components:
|
||||
$ref: '#/components/schemas/DashboardtypesDatasourceSpec'
|
||||
type: object
|
||||
display:
|
||||
$ref: '#/components/schemas/DashboardtypesDisplay'
|
||||
$ref: '#/components/schemas/CommonDisplay'
|
||||
duration:
|
||||
type: string
|
||||
layouts:
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardtypesLayout'
|
||||
nullable: true
|
||||
type: array
|
||||
links:
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardLink'
|
||||
$ref: '#/components/schemas/V1Link'
|
||||
type: array
|
||||
panels:
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/DashboardtypesPanel'
|
||||
nullable: true
|
||||
type: object
|
||||
refreshInterval:
|
||||
type: string
|
||||
@@ -2573,19 +2539,10 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardtypesVariable'
|
||||
type: array
|
||||
required:
|
||||
- display
|
||||
- variables
|
||||
- panels
|
||||
- layouts
|
||||
- duration
|
||||
type: object
|
||||
DashboardtypesDatasourcePlugin:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/DashboardtypesDatasourcePluginVariantStruct'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
DashboardtypesDatasourcePluginKind:
|
||||
enum:
|
||||
- signoz/Datasource
|
||||
@@ -2612,15 +2569,6 @@ components:
|
||||
plugin:
|
||||
$ref: '#/components/schemas/DashboardtypesDatasourcePlugin'
|
||||
type: object
|
||||
DashboardtypesDisplay:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
DashboardtypesDynamicVariableSpec:
|
||||
properties:
|
||||
name:
|
||||
@@ -2739,11 +2687,8 @@ components:
|
||||
type: object
|
||||
DashboardtypesLayout:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/DashboardtypesLayoutEnvelopeGithubComPersesSpecGoDashboardGridLayoutSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
DashboardtypesLayoutEnvelopeGithubComPersesSpecGoDashboardGridLayoutSpec:
|
||||
- $ref: '#/components/schemas/DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpec'
|
||||
DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
@@ -2802,7 +2747,7 @@ components:
|
||||
defaultValue:
|
||||
$ref: '#/components/schemas/VariableDefaultValue'
|
||||
display:
|
||||
$ref: '#/components/schemas/DashboardtypesDisplay'
|
||||
$ref: '#/components/schemas/VariableDisplay'
|
||||
name:
|
||||
type: string
|
||||
plugin:
|
||||
@@ -2810,8 +2755,6 @@ components:
|
||||
sort:
|
||||
nullable: true
|
||||
type: string
|
||||
required:
|
||||
- display
|
||||
type: object
|
||||
DashboardtypesNumberPanelSpec:
|
||||
properties:
|
||||
@@ -2828,12 +2771,9 @@ components:
|
||||
DashboardtypesPanel:
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/DashboardtypesPanelKind'
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/DashboardtypesPanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
DashboardtypesPanelFormatting:
|
||||
properties:
|
||||
@@ -2842,10 +2782,6 @@ components:
|
||||
unit:
|
||||
type: string
|
||||
type: object
|
||||
DashboardtypesPanelKind:
|
||||
enum:
|
||||
- Panel
|
||||
type: string
|
||||
DashboardtypesPanelPlugin:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec'
|
||||
@@ -2855,9 +2791,6 @@ components:
|
||||
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec'
|
||||
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec'
|
||||
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
DashboardtypesPanelPluginKind:
|
||||
enum:
|
||||
- signoz/TimeSeriesPanel
|
||||
@@ -2955,22 +2888,17 @@ components:
|
||||
DashboardtypesPanelSpec:
|
||||
properties:
|
||||
display:
|
||||
$ref: '#/components/schemas/DashboardtypesDisplay'
|
||||
$ref: '#/components/schemas/V1PanelDisplay'
|
||||
links:
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardLink'
|
||||
$ref: '#/components/schemas/V1Link'
|
||||
type: array
|
||||
plugin:
|
||||
$ref: '#/components/schemas/DashboardtypesPanelPlugin'
|
||||
queries:
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardtypesQuery'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- display
|
||||
- plugin
|
||||
- queries
|
||||
type: object
|
||||
DashboardtypesPatchOp:
|
||||
enum:
|
||||
@@ -3036,12 +2964,9 @@ components:
|
||||
DashboardtypesQuery:
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5RequestType'
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/DashboardtypesQuerySpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
DashboardtypesQueryPlugin:
|
||||
oneOf:
|
||||
@@ -3051,9 +2976,6 @@ components:
|
||||
- $ref: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5PromQuery'
|
||||
- $ref: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5ClickHouseQuery'
|
||||
- $ref: '#/components/schemas/DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5QueryBuilderTraceOperator'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
DashboardtypesQueryPluginKind:
|
||||
enum:
|
||||
- signoz/BuilderQuery
|
||||
@@ -3141,8 +3063,6 @@ components:
|
||||
type: string
|
||||
plugin:
|
||||
$ref: '#/components/schemas/DashboardtypesQueryPlugin'
|
||||
required:
|
||||
- plugin
|
||||
type: object
|
||||
DashboardtypesQueryVariableSpec:
|
||||
properties:
|
||||
@@ -3312,11 +3232,8 @@ components:
|
||||
DashboardtypesVariable:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/DashboardtypesVariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesListVariableSpec'
|
||||
- $ref: '#/components/schemas/DashboardtypesVariableEnvelopeGithubComPersesSpecGoDashboardTextVariableSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
DashboardtypesVariableEnvelopeGithubComPersesSpecGoDashboardTextVariableSpec:
|
||||
- $ref: '#/components/schemas/DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpec'
|
||||
DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
@@ -3345,9 +3262,6 @@ components:
|
||||
- $ref: '#/components/schemas/DashboardtypesVariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDynamicVariableSpec'
|
||||
- $ref: '#/components/schemas/DashboardtypesVariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesQueryVariableSpec'
|
||||
- $ref: '#/components/schemas/DashboardtypesVariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesCustomVariableSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
DashboardtypesVariablePluginKind:
|
||||
enum:
|
||||
- signoz/DynamicVariable
|
||||
@@ -7337,6 +7251,26 @@ components:
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
V1Link:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
renderVariables:
|
||||
type: boolean
|
||||
targetBlank:
|
||||
type: boolean
|
||||
tooltip:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
V1PanelDisplay:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
VariableDefaultValue:
|
||||
type: object
|
||||
VariableDisplay:
|
||||
@@ -7753,7 +7687,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AlertmanagertypesReceiver'
|
||||
$ref: '#/components/schemas/ConfigReceiver'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
@@ -7804,7 +7738,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AlertmanagertypesReceiver'
|
||||
$ref: '#/components/schemas/ConfigReceiver'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
@@ -8228,80 +8162,6 @@ paths:
|
||||
tags:
|
||||
- cloudintegration
|
||||
/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}/services/{service_id}:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint gets a service and its configuration for the specified
|
||||
cloud integration account
|
||||
operationId: GetAccountService
|
||||
parameters:
|
||||
- in: path
|
||||
name: cloud_provider
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: service_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesService'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Get service for account
|
||||
tags:
|
||||
- cloudintegration
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint updates a service for the specified cloud provider
|
||||
@@ -12542,7 +12402,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AlertmanagertypesReceiver'
|
||||
$ref: '#/components/schemas/ConfigReceiver'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
|
||||
@@ -3,36 +3,13 @@ import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
export interface DayBreakdownEntry {
|
||||
timestamp: number;
|
||||
total: number;
|
||||
quantity: number;
|
||||
count: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface TierEntry {
|
||||
quantity: number;
|
||||
unitPrice: number;
|
||||
tierCost: number;
|
||||
}
|
||||
|
||||
export interface BreakdownEntry {
|
||||
type: string;
|
||||
unit: string;
|
||||
dayWiseBreakdown: {
|
||||
breakdown: DayBreakdownEntry[];
|
||||
};
|
||||
tiers?: TierEntry[];
|
||||
}
|
||||
|
||||
export interface UsageResponsePayloadProps {
|
||||
billingPeriodStart: number;
|
||||
billingPeriodEnd: number;
|
||||
billingPeriodStart: Date;
|
||||
billingPeriodEnd: Date;
|
||||
details: {
|
||||
total: number;
|
||||
baseFee: number;
|
||||
breakdown: BreakdownEntry[];
|
||||
breakdown: [];
|
||||
billTotal: number;
|
||||
};
|
||||
discount: number;
|
||||
|
||||
@@ -19,7 +19,7 @@ import type {
|
||||
|
||||
import type {
|
||||
AlertmanagertypesPostableChannelDTO,
|
||||
AlertmanagertypesReceiverDTO,
|
||||
ConfigReceiverDTO,
|
||||
CreateChannel201,
|
||||
DeleteChannelByIDPathParameters,
|
||||
GetChannelByID200,
|
||||
@@ -385,14 +385,14 @@ export const invalidateGetChannelByID = async (
|
||||
*/
|
||||
export const updateChannelByID = (
|
||||
{ id }: UpdateChannelByIDPathParameters,
|
||||
alertmanagertypesReceiverDTO?: BodyType<AlertmanagertypesReceiverDTO>,
|
||||
configReceiverDTO?: BodyType<ConfigReceiverDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/channels/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: alertmanagertypesReceiverDTO,
|
||||
data: configReceiverDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
@@ -406,7 +406,7 @@ export const getUpdateChannelByIDMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateChannelByIDPathParameters;
|
||||
data?: BodyType<AlertmanagertypesReceiverDTO>;
|
||||
data?: BodyType<ConfigReceiverDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -415,7 +415,7 @@ export const getUpdateChannelByIDMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateChannelByIDPathParameters;
|
||||
data?: BodyType<AlertmanagertypesReceiverDTO>;
|
||||
data?: BodyType<ConfigReceiverDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
@@ -432,7 +432,7 @@ export const getUpdateChannelByIDMutationOptions = <
|
||||
Awaited<ReturnType<typeof updateChannelByID>>,
|
||||
{
|
||||
pathParams: UpdateChannelByIDPathParameters;
|
||||
data?: BodyType<AlertmanagertypesReceiverDTO>;
|
||||
data?: BodyType<ConfigReceiverDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
@@ -447,7 +447,7 @@ export type UpdateChannelByIDMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateChannelByID>>
|
||||
>;
|
||||
export type UpdateChannelByIDMutationBody =
|
||||
| BodyType<AlertmanagertypesReceiverDTO>
|
||||
| BodyType<ConfigReceiverDTO>
|
||||
| undefined;
|
||||
export type UpdateChannelByIDMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
@@ -463,7 +463,7 @@ export const useUpdateChannelByID = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateChannelByIDPathParameters;
|
||||
data?: BodyType<AlertmanagertypesReceiverDTO>;
|
||||
data?: BodyType<ConfigReceiverDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -472,7 +472,7 @@ export const useUpdateChannelByID = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateChannelByIDPathParameters;
|
||||
data?: BodyType<AlertmanagertypesReceiverDTO>;
|
||||
data?: BodyType<ConfigReceiverDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
@@ -483,14 +483,14 @@ export const useUpdateChannelByID = <
|
||||
* @summary Test notification channel
|
||||
*/
|
||||
export const testChannel = (
|
||||
alertmanagertypesReceiverDTO?: BodyType<AlertmanagertypesReceiverDTO>,
|
||||
configReceiverDTO?: BodyType<ConfigReceiverDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/channels/test`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: alertmanagertypesReceiverDTO,
|
||||
data: configReceiverDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
@@ -502,13 +502,13 @@ export const getTestChannelMutationOptions = <
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testChannel>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
|
||||
{ data?: BodyType<ConfigReceiverDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testChannel>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
|
||||
{ data?: BodyType<ConfigReceiverDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['testChannel'];
|
||||
@@ -522,7 +522,7 @@ export const getTestChannelMutationOptions = <
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof testChannel>>,
|
||||
{ data?: BodyType<AlertmanagertypesReceiverDTO> }
|
||||
{ data?: BodyType<ConfigReceiverDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
@@ -535,9 +535,7 @@ export const getTestChannelMutationOptions = <
|
||||
export type TestChannelMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof testChannel>>
|
||||
>;
|
||||
export type TestChannelMutationBody =
|
||||
| BodyType<AlertmanagertypesReceiverDTO>
|
||||
| undefined;
|
||||
export type TestChannelMutationBody = BodyType<ConfigReceiverDTO> | undefined;
|
||||
export type TestChannelMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
@@ -550,13 +548,13 @@ export const useTestChannel = <
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testChannel>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
|
||||
{ data?: BodyType<ConfigReceiverDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof testChannel>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
|
||||
{ data?: BodyType<ConfigReceiverDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getTestChannelMutationOptions(options));
|
||||
@@ -567,14 +565,14 @@ export const useTestChannel = <
|
||||
* @summary Test notification channel (deprecated)
|
||||
*/
|
||||
export const testChannelDeprecated = (
|
||||
alertmanagertypesReceiverDTO?: BodyType<AlertmanagertypesReceiverDTO>,
|
||||
configReceiverDTO?: BodyType<ConfigReceiverDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/testChannel`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: alertmanagertypesReceiverDTO,
|
||||
data: configReceiverDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
@@ -586,13 +584,13 @@ export const getTestChannelDeprecatedMutationOptions = <
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testChannelDeprecated>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
|
||||
{ data?: BodyType<ConfigReceiverDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testChannelDeprecated>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
|
||||
{ data?: BodyType<ConfigReceiverDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['testChannelDeprecated'];
|
||||
@@ -606,7 +604,7 @@ export const getTestChannelDeprecatedMutationOptions = <
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof testChannelDeprecated>>,
|
||||
{ data?: BodyType<AlertmanagertypesReceiverDTO> }
|
||||
{ data?: BodyType<ConfigReceiverDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
@@ -620,7 +618,7 @@ export type TestChannelDeprecatedMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof testChannelDeprecated>>
|
||||
>;
|
||||
export type TestChannelDeprecatedMutationBody =
|
||||
| BodyType<AlertmanagertypesReceiverDTO>
|
||||
| BodyType<ConfigReceiverDTO>
|
||||
| undefined;
|
||||
export type TestChannelDeprecatedMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
@@ -636,13 +634,13 @@ export const useTestChannelDeprecated = <
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testChannelDeprecated>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
|
||||
{ data?: BodyType<ConfigReceiverDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof testChannelDeprecated>>,
|
||||
TError,
|
||||
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
|
||||
{ data?: BodyType<ConfigReceiverDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getTestChannelDeprecatedMutationOptions(options));
|
||||
|
||||
@@ -31,8 +31,6 @@ import type {
|
||||
DisconnectAccountPathParameters,
|
||||
GetAccount200,
|
||||
GetAccountPathParameters,
|
||||
GetAccountService200,
|
||||
GetAccountServicePathParameters,
|
||||
GetConnectionCredentials200,
|
||||
GetConnectionCredentialsPathParameters,
|
||||
GetService200,
|
||||
@@ -745,117 +743,6 @@ export const invalidateListAccountServicesMetadata = async (
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint gets a service and its configuration for the specified cloud integration account
|
||||
* @summary Get service for account
|
||||
*/
|
||||
export const getAccountService = (
|
||||
{ cloudProvider, id, serviceId }: GetAccountServicePathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetAccountService200>({
|
||||
url: `/api/v1/cloud_integrations/${cloudProvider}/accounts/${id}/services/${serviceId}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetAccountServiceQueryKey = ({
|
||||
cloudProvider,
|
||||
id,
|
||||
serviceId,
|
||||
}: GetAccountServicePathParameters) => {
|
||||
return [
|
||||
`/api/v1/cloud_integrations/${cloudProvider}/accounts/${id}/services/${serviceId}`,
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const getGetAccountServiceQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getAccountService>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ cloudProvider, id, serviceId }: GetAccountServicePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getAccountService>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ??
|
||||
getGetAccountServiceQueryKey({ cloudProvider, id, serviceId });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getAccountService>>
|
||||
> = ({ signal }) =>
|
||||
getAccountService({ cloudProvider, id, serviceId }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!(cloudProvider && id && serviceId),
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getAccountService>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetAccountServiceQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getAccountService>>
|
||||
>;
|
||||
export type GetAccountServiceQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get service for account
|
||||
*/
|
||||
|
||||
export function useGetAccountService<
|
||||
TData = Awaited<ReturnType<typeof getAccountService>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ cloudProvider, id, serviceId }: GetAccountServicePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getAccountService>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetAccountServiceQueryOptions(
|
||||
{ cloudProvider, id, serviceId },
|
||||
options,
|
||||
);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get service for account
|
||||
*/
|
||||
export const invalidateGetAccountService = async (
|
||||
queryClient: QueryClient,
|
||||
{ cloudProvider, id, serviceId }: GetAccountServicePathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetAccountServiceQueryKey({ cloudProvider, id, serviceId }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint updates a service for the specified cloud provider
|
||||
* @summary Update service
|
||||
|
||||
@@ -134,6 +134,113 @@ export interface AlertmanagertypesGettableRoutePolicyDTO {
|
||||
updatedBy?: string | null;
|
||||
}
|
||||
|
||||
export enum AlertmanagertypesMaintenanceKindDTO {
|
||||
fixed = 'fixed',
|
||||
recurring = 'recurring',
|
||||
}
|
||||
export enum AlertmanagertypesMaintenanceStatusDTO {
|
||||
active = 'active',
|
||||
upcoming = 'upcoming',
|
||||
expired = 'expired',
|
||||
}
|
||||
export enum AlertmanagertypesRepeatOnDTO {
|
||||
sunday = 'sunday',
|
||||
monday = 'monday',
|
||||
tuesday = 'tuesday',
|
||||
wednesday = 'wednesday',
|
||||
thursday = 'thursday',
|
||||
friday = 'friday',
|
||||
saturday = 'saturday',
|
||||
}
|
||||
export enum AlertmanagertypesRepeatTypeDTO {
|
||||
daily = 'daily',
|
||||
weekly = 'weekly',
|
||||
monthly = 'monthly',
|
||||
}
|
||||
export interface AlertmanagertypesRecurrenceDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
duration: string;
|
||||
/**
|
||||
* @type string,null
|
||||
* @format date-time
|
||||
*/
|
||||
endTime?: string | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
repeatOn?: AlertmanagertypesRepeatOnDTO[] | null;
|
||||
repeatType: AlertmanagertypesRepeatTypeDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
startTime: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesScheduleDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
endTime?: string;
|
||||
recurrence?: AlertmanagertypesRecurrenceDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
startTime?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesPlannedMaintenanceDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
alertIds?: string[] | null;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
kind: AlertmanagertypesMaintenanceKindDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
schedule: AlertmanagertypesScheduleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
scope?: string;
|
||||
status: AlertmanagertypesMaintenanceStatusDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface ConfigAuthorizationDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -368,130 +475,6 @@ export interface ConfigSecretURLDTO {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesGoogleChatReceiverConfigDTO {
|
||||
http_config?: ConfigHTTPClientConfigDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
send_resolved?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
text?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
title?: string;
|
||||
webhook_url?: ConfigSecretURLDTO;
|
||||
}
|
||||
|
||||
export enum AlertmanagertypesMaintenanceKindDTO {
|
||||
fixed = 'fixed',
|
||||
recurring = 'recurring',
|
||||
}
|
||||
export enum AlertmanagertypesMaintenanceStatusDTO {
|
||||
active = 'active',
|
||||
upcoming = 'upcoming',
|
||||
expired = 'expired',
|
||||
}
|
||||
export enum AlertmanagertypesRepeatOnDTO {
|
||||
sunday = 'sunday',
|
||||
monday = 'monday',
|
||||
tuesday = 'tuesday',
|
||||
wednesday = 'wednesday',
|
||||
thursday = 'thursday',
|
||||
friday = 'friday',
|
||||
saturday = 'saturday',
|
||||
}
|
||||
export enum AlertmanagertypesRepeatTypeDTO {
|
||||
daily = 'daily',
|
||||
weekly = 'weekly',
|
||||
monthly = 'monthly',
|
||||
}
|
||||
export interface AlertmanagertypesRecurrenceDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
duration: string;
|
||||
/**
|
||||
* @type string,null
|
||||
* @format date-time
|
||||
*/
|
||||
endTime?: string | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
repeatOn?: AlertmanagertypesRepeatOnDTO[] | null;
|
||||
repeatType: AlertmanagertypesRepeatTypeDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
startTime: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesScheduleDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
endTime?: string;
|
||||
recurrence?: AlertmanagertypesRecurrenceDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
startTime?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesPlannedMaintenanceDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
alertIds?: string[] | null;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
kind: AlertmanagertypesMaintenanceKindDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
schedule: AlertmanagertypesScheduleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
scope?: string;
|
||||
status: AlertmanagertypesMaintenanceStatusDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface ConfigDiscordConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -1651,10 +1634,6 @@ export type AlertmanagertypesPostableChannelDTO = unknown & {
|
||||
* @type array
|
||||
*/
|
||||
email_configs?: ConfigEmailConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
googlechat_configs?: AlertmanagertypesGoogleChatReceiverConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
@@ -1769,89 +1748,6 @@ export interface AlertmanagertypesPostableRoutePolicyDTO {
|
||||
tags?: string[] | null;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesReceiverDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
discord_configs?: ConfigDiscordConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
email_configs?: ConfigEmailConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
googlechat_configs?: AlertmanagertypesGoogleChatReceiverConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
incidentio_configs?: ConfigIncidentioConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
jira_configs?: ConfigJiraConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
mattermost_configs?: ConfigMattermostConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
msteams_configs?: ConfigMSTeamsConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
msteamsv2_configs?: ConfigMSTeamsV2ConfigDTO[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
opsgenie_configs?: ConfigOpsGenieConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
pagerduty_configs?: ConfigPagerdutyConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
pushover_configs?: ConfigPushoverConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
rocketchat_configs?: ConfigRocketchatConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
slack_configs?: ConfigSlackConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
sns_configs?: ConfigSNSConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
telegram_configs?: ConfigTelegramConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
victorops_configs?: ConfigVictorOpsConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
webex_configs?: ConfigWebexConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
webhook_configs?: ConfigWebhookConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
wechat_configs?: ConfigWechatConfigDTO[];
|
||||
}
|
||||
|
||||
export interface AuthtypesAttributeMappingDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -3063,6 +2959,85 @@ export interface CommonJSONRefDTO {
|
||||
$ref?: string;
|
||||
}
|
||||
|
||||
export interface ConfigReceiverDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
discord_configs?: ConfigDiscordConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
email_configs?: ConfigEmailConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
incidentio_configs?: ConfigIncidentioConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
jira_configs?: ConfigJiraConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
mattermost_configs?: ConfigMattermostConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
msteams_configs?: ConfigMSTeamsConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
msteamsv2_configs?: ConfigMSTeamsV2ConfigDTO[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
opsgenie_configs?: ConfigOpsGenieConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
pagerduty_configs?: ConfigPagerdutyConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
pushover_configs?: ConfigPushoverConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
rocketchat_configs?: ConfigRocketchatConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
slack_configs?: ConfigSlackConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
sns_configs?: ConfigSNSConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
telegram_configs?: ConfigTelegramConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
victorops_configs?: ConfigVictorOpsConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
webex_configs?: ConfigWebexConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
webhook_configs?: ConfigWebhookConfigDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
wechat_configs?: ConfigWechatConfigDTO[];
|
||||
}
|
||||
|
||||
export interface CoretypesObjectGroupDTO {
|
||||
resource: CoretypesResourceRefDTO;
|
||||
/**
|
||||
@@ -3129,29 +3104,6 @@ export interface DashboardGridLayoutSpecDTO {
|
||||
repeatVariable?: string;
|
||||
}
|
||||
|
||||
export interface DashboardLinkDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
renderVariables?: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
targetBlank?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
tooltip?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface VariableDisplayDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -3853,10 +3805,7 @@ export type DashboardtypesDashboardSpecDTODatasources = {
|
||||
[key: string]: DashboardtypesDatasourceSpecDTO;
|
||||
};
|
||||
|
||||
export enum DashboardtypesPanelKindDTO {
|
||||
Panel = 'Panel',
|
||||
}
|
||||
export interface DashboardtypesDisplayDTO {
|
||||
export interface V1PanelDisplayDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -3864,7 +3813,30 @@ export interface DashboardtypesDisplayDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface V1LinkDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
renderVariables?: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
targetBlank?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
tooltip?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export enum DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpecDTOKind {
|
||||
@@ -4108,13 +4080,6 @@ export type DashboardtypesPanelPluginDTO =
|
||||
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpecDTO
|
||||
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpecDTO;
|
||||
|
||||
export enum Querybuildertypesv5RequestTypeDTO {
|
||||
scalar = 'scalar',
|
||||
time_series = 'time_series',
|
||||
raw = 'raw',
|
||||
raw_stream = 'raw_stream',
|
||||
trace = 'trace',
|
||||
}
|
||||
export enum DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBuilderQuerySpecDTOKind {
|
||||
'signoz/BuilderQuery' = 'signoz/BuilderQuery',
|
||||
}
|
||||
@@ -4415,50 +4380,62 @@ export interface DashboardtypesQuerySpecDTO {
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
plugin: DashboardtypesQueryPluginDTO;
|
||||
plugin?: DashboardtypesQueryPluginDTO;
|
||||
}
|
||||
|
||||
export interface DashboardtypesQueryDTO {
|
||||
kind: Querybuildertypesv5RequestTypeDTO;
|
||||
spec: DashboardtypesQuerySpecDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
kind?: string;
|
||||
spec?: DashboardtypesQuerySpecDTO;
|
||||
}
|
||||
|
||||
export interface DashboardtypesPanelSpecDTO {
|
||||
display: DashboardtypesDisplayDTO;
|
||||
display?: V1PanelDisplayDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
links?: DashboardLinkDTO[];
|
||||
plugin: DashboardtypesPanelPluginDTO;
|
||||
links?: V1LinkDTO[];
|
||||
plugin?: DashboardtypesPanelPluginDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
* @type array
|
||||
*/
|
||||
queries: DashboardtypesQueryDTO[] | null;
|
||||
queries?: DashboardtypesQueryDTO[];
|
||||
}
|
||||
|
||||
export interface DashboardtypesPanelDTO {
|
||||
kind: DashboardtypesPanelKindDTO;
|
||||
spec: DashboardtypesPanelSpecDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
kind?: string;
|
||||
spec?: DashboardtypesPanelSpecDTO;
|
||||
}
|
||||
|
||||
export type DashboardtypesDashboardSpecDTOPanels = {
|
||||
export type DashboardtypesDashboardSpecDTOPanelsAnyOf = {
|
||||
[key: string]: DashboardtypesPanelDTO;
|
||||
};
|
||||
|
||||
export enum DashboardtypesLayoutEnvelopeGithubComPersesSpecGoDashboardGridLayoutSpecDTOKind {
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type DashboardtypesDashboardSpecDTOPanels =
|
||||
DashboardtypesDashboardSpecDTOPanelsAnyOf | null;
|
||||
|
||||
export enum DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpecDTOKind {
|
||||
Grid = 'Grid',
|
||||
}
|
||||
export interface DashboardtypesLayoutEnvelopeGithubComPersesSpecGoDashboardGridLayoutSpecDTO {
|
||||
export interface DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpecDTO {
|
||||
/**
|
||||
* @enum Grid
|
||||
* @type string
|
||||
*/
|
||||
kind: DashboardtypesLayoutEnvelopeGithubComPersesSpecGoDashboardGridLayoutSpecDTOKind;
|
||||
kind: DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpecDTOKind;
|
||||
spec: DashboardGridLayoutSpecDTO;
|
||||
}
|
||||
|
||||
export type DashboardtypesLayoutDTO =
|
||||
DashboardtypesLayoutEnvelopeGithubComPersesSpecGoDashboardGridLayoutSpecDTO;
|
||||
DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpecDTO;
|
||||
|
||||
export enum DashboardtypesVariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesListVariableSpecDTOKind {
|
||||
ListVariable = 'ListVariable',
|
||||
@@ -4541,7 +4518,7 @@ export interface DashboardtypesListVariableSpecDTO {
|
||||
*/
|
||||
customAllValue?: string;
|
||||
defaultValue?: VariableDefaultValueDTO;
|
||||
display: DashboardtypesDisplayDTO;
|
||||
display?: VariableDisplayDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -4562,44 +4539,44 @@ export interface DashboardtypesVariableEnvelopeGithubComSigNozSignozPkgTypesDash
|
||||
spec: DashboardtypesListVariableSpecDTO;
|
||||
}
|
||||
|
||||
export enum DashboardtypesVariableEnvelopeGithubComPersesSpecGoDashboardTextVariableSpecDTOKind {
|
||||
export enum DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpecDTOKind {
|
||||
TextVariable = 'TextVariable',
|
||||
}
|
||||
export interface DashboardtypesVariableEnvelopeGithubComPersesSpecGoDashboardTextVariableSpecDTO {
|
||||
export interface DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpecDTO {
|
||||
/**
|
||||
* @enum TextVariable
|
||||
* @type string
|
||||
*/
|
||||
kind: DashboardtypesVariableEnvelopeGithubComPersesSpecGoDashboardTextVariableSpecDTOKind;
|
||||
kind: DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpecDTOKind;
|
||||
spec: DashboardTextVariableSpecDTO;
|
||||
}
|
||||
|
||||
export type DashboardtypesVariableDTO =
|
||||
| DashboardtypesVariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesListVariableSpecDTO
|
||||
| DashboardtypesVariableEnvelopeGithubComPersesSpecGoDashboardTextVariableSpecDTO;
|
||||
| DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpecDTO;
|
||||
|
||||
export interface DashboardtypesDashboardSpecDTO {
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
datasources?: DashboardtypesDashboardSpecDTODatasources;
|
||||
display: DashboardtypesDisplayDTO;
|
||||
display?: CommonDisplayDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
duration: string;
|
||||
duration?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
layouts?: DashboardtypesLayoutDTO[] | null;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
layouts: DashboardtypesLayoutDTO[];
|
||||
links?: V1LinkDTO[];
|
||||
/**
|
||||
* @type array
|
||||
* @type object,null
|
||||
*/
|
||||
links?: DashboardLinkDTO[];
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
panels: DashboardtypesDashboardSpecDTOPanels;
|
||||
panels?: DashboardtypesDashboardSpecDTOPanels;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -4607,7 +4584,7 @@ export interface DashboardtypesDashboardSpecDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
variables: DashboardtypesVariableDTO[];
|
||||
variables?: DashboardtypesVariableDTO[];
|
||||
}
|
||||
|
||||
export enum DashboardtypesDatasourcePluginKindDTO {
|
||||
@@ -6977,6 +6954,13 @@ export type Querybuildertypesv5QueryRangeRequestDTOVariables = {
|
||||
[key: string]: Querybuildertypesv5VariableItemDTO;
|
||||
};
|
||||
|
||||
export enum Querybuildertypesv5RequestTypeDTO {
|
||||
scalar = 'scalar',
|
||||
time_series = 'time_series',
|
||||
raw = 'raw',
|
||||
raw_stream = 'raw_stream',
|
||||
trace = 'trace',
|
||||
}
|
||||
/**
|
||||
* Request body for the v5 query range endpoint. Supports builder queries (traces, logs, metrics), formulas, joins, trace operators, PromQL, and ClickHouse SQL queries.
|
||||
*/
|
||||
@@ -8723,19 +8707,6 @@ export type ListAccountServicesMetadata200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetAccountServicePathParameters = {
|
||||
cloudProvider: string;
|
||||
id: string;
|
||||
serviceId: string;
|
||||
};
|
||||
export type GetAccountService200 = {
|
||||
data: CloudintegrationtypesServiceDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateServicePathParameters = {
|
||||
cloudProvider: string;
|
||||
id: string;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import refreshPaymentStatus from 'api/v3/licenses/put';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import cx from 'classnames';
|
||||
import { RefreshCcw } from '@signozhq/icons';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
function RefreshPaymentStatus({
|
||||
btnShape,
|
||||
type,
|
||||
className,
|
||||
}: {
|
||||
btnShape?: 'default' | 'round' | 'circle';
|
||||
type?: 'button' | 'text' | 'tooltip';
|
||||
className?: string;
|
||||
}): JSX.Element {
|
||||
const { t } = useTranslation(['failedPayment']);
|
||||
const { activeLicenseRefetch } = useAppContext();
|
||||
@@ -31,33 +31,26 @@ function RefreshPaymentStatus({
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
variant="link"
|
||||
color={type === 'text' ? 'none' : 'secondary'}
|
||||
size="md"
|
||||
className={className}
|
||||
onClick={handleRefreshPaymentStatus}
|
||||
prefix={<RefreshCcw size={14} />}
|
||||
loading={isLoading}
|
||||
>
|
||||
{type !== 'tooltip' ? t('refreshPaymentStatus') : ''}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<span className="refresh-payment-status-btn-wrapper">
|
||||
{type === 'tooltip' ? (
|
||||
<TooltipSimple title={t('refreshPaymentStatus')}>{button}</TooltipSimple>
|
||||
) : (
|
||||
button
|
||||
)}
|
||||
<Tooltip title={type === 'tooltip' ? t('refreshPaymentStatus') : ''}>
|
||||
<Button
|
||||
type={type === 'text' ? 'text' : 'default'}
|
||||
shape={btnShape}
|
||||
className={cx('periscope-btn', { text: type === 'text' })}
|
||||
onClick={handleRefreshPaymentStatus}
|
||||
icon={<RefreshCcw size={14} />}
|
||||
loading={isLoading}
|
||||
>
|
||||
{type !== 'tooltip' ? t('refreshPaymentStatus') : ''}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
RefreshPaymentStatus.defaultProps = {
|
||||
btnShape: 'default',
|
||||
type: 'button',
|
||||
className: undefined,
|
||||
};
|
||||
|
||||
export default RefreshPaymentStatus;
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
.billingContainer {
|
||||
margin-bottom: var(--spacing-20);
|
||||
padding-top: 36px;
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
|
||||
.pageHeader {
|
||||
margin-bottom: var(--spacing-8);
|
||||
|
||||
.pageHeaderTitle {
|
||||
font-weight: var(--label-medium-500-font-weight);
|
||||
font-size: var(--label-medium-500-font-size);
|
||||
line-height: 32px;
|
||||
letter-spacing: -0.08px;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.pageHeaderSubtitle {
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-20);
|
||||
letter-spacing: -0.07px;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.pageInfoTitle {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-20);
|
||||
letter-spacing: -0.07px;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.pageInfoSubtitle {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: var(--line-height-18);
|
||||
letter-spacing: -0.07px;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.pageInfo {
|
||||
:global(.ant-card) {
|
||||
padding: var(--padding-3);
|
||||
}
|
||||
|
||||
.billingManageBtn {
|
||||
background: var(--l3-background);
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.billingSummary {
|
||||
margin: var(--spacing-12) var(--spacing-4);
|
||||
}
|
||||
|
||||
.billingDetails {
|
||||
margin: var(--spacing-12) 0;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
:global {
|
||||
.ant-table {
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
height: 52px;
|
||||
padding: 0 var(--padding-4);
|
||||
color: var(--l3-foreground);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
letter-spacing: 0.48px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
height: 52px;
|
||||
padding: 0 var(--padding-4);
|
||||
background: var(--l2-background);
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
&:first-child {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
font-feature-settings:
|
||||
'zero' 1,
|
||||
'lnum' 1,
|
||||
'tnum' 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:last-child > td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: var(--l2-background) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.billingDetailsHeaderCell {
|
||||
position: relative;
|
||||
background: var(--l2-background) !important;
|
||||
border: none !important;
|
||||
border-bottom: 1px solid var(--l1-border) !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-block: 0;
|
||||
inset-inline-end: 0;
|
||||
width: 2px;
|
||||
background: var(--l2-background);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upgradePlanBenefits {
|
||||
margin: 0 var(--spacing-4);
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 5px;
|
||||
padding: 0 var(--padding-12);
|
||||
|
||||
.planBenefits {
|
||||
.planBenefit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
margin: var(--spacing-8) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.billingGraphSection {
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--spacing-4);
|
||||
|
||||
.billingGraphFooter {
|
||||
display: flex;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--padding-3) var(--padding-4);
|
||||
border-top: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
|
||||
.billingFooterBtn {
|
||||
background: var(--l3-background);
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emptyGraphCard {
|
||||
:global(.ant-card-body) {
|
||||
height: 40vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.billingUpdateNote {
|
||||
margin-top: var(--spacing-8);
|
||||
font-family: var(--font-family-inter);
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
:global {
|
||||
.ant-skeleton.ant-skeleton-element.ant-skeleton-active {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.ant-skeleton.ant-skeleton-element .ant-skeleton-input {
|
||||
min-width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
.billing-container {
|
||||
margin-bottom: 40px;
|
||||
padding-top: 36px;
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
|
||||
.billing-summary {
|
||||
margin: 24px 8px;
|
||||
}
|
||||
|
||||
.billing-details {
|
||||
margin: 24px 0px;
|
||||
|
||||
.ant-table-title {
|
||||
color: var(--l2-foreground);
|
||||
background-color: var(--l3-background);
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
background-color: var(--l1-background);
|
||||
border-color: var(--l1-border);
|
||||
}
|
||||
|
||||
.ant-table-tbody {
|
||||
td {
|
||||
border-color: var(--l1-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upgrade-plan-benefits {
|
||||
margin: 0px 8px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 5px;
|
||||
padding: 0 48px;
|
||||
.plan-benefits {
|
||||
.plan-benefit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-graph-card {
|
||||
.ant-card-body {
|
||||
height: 40vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.billing-update-note {
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
color: var(--l2-foreground);
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-skeleton.ant-skeleton-element.ant-skeleton-active {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.ant-skeleton.ant-skeleton-element .ant-skeleton-input {
|
||||
min-width: 100% !important;
|
||||
}
|
||||
@@ -38,7 +38,7 @@ describe('BillingContainer', () => {
|
||||
});
|
||||
expect(pricePerUnit).toBeInTheDocument();
|
||||
const cost = await screen.findByRole('columnheader', {
|
||||
name: /cost/i,
|
||||
name: /cost \(billing period to date\)/i,
|
||||
});
|
||||
expect(cost).toBeInTheDocument();
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Callout } from '@signozhq/ui/callout';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { CircleCheck, Landmark, MonitorDown } from '@signozhq/icons';
|
||||
import { CircleCheck, CloudDownload } from '@signozhq/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Flex,
|
||||
@@ -15,10 +16,7 @@ import {
|
||||
TableColumnsType as ColumnsType,
|
||||
} from 'antd';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import getUsage, {
|
||||
BreakdownEntry,
|
||||
UsageResponsePayloadProps,
|
||||
} from 'api/billing/getUsage';
|
||||
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import updateCreditCardApi from 'api/v1/checkout/create';
|
||||
import manageCreditCardApi from 'api/v1/portal/create';
|
||||
@@ -31,7 +29,7 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { isEmpty, pick } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { ErrorResponse, SuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
||||
@@ -40,7 +38,7 @@ import CancelSubscriptionBanner from './CancelSubscriptionBanner';
|
||||
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
|
||||
import { prepareCsvData } from './BillingUsageGraph/utils';
|
||||
|
||||
import styles from './BillingContainer.module.scss';
|
||||
import './BillingContainer.styles.scss';
|
||||
import { LicenseState } from 'types/api/licensesV3/getActive';
|
||||
|
||||
interface DataType {
|
||||
@@ -117,7 +115,7 @@ const dummyColumns: ColumnsType<DataType> = [
|
||||
render: renderSkeletonInput,
|
||||
},
|
||||
{
|
||||
title: 'Cost',
|
||||
title: 'Cost (Billing period to date)',
|
||||
dataIndex: 'cost',
|
||||
key: 'cost',
|
||||
render: renderSkeletonInput,
|
||||
@@ -132,7 +130,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
const [billAmount, setBillAmount] = useState(0);
|
||||
const [daysRemaining, setDaysRemaining] = useState(0);
|
||||
const [isFreeTrial, setIsFreeTrial] = useState(false);
|
||||
const [data, setData] = useState<DataType[]>([]);
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [apiResponse, setApiResponse] = useState<
|
||||
Partial<UsageResponsePayloadProps>
|
||||
>({});
|
||||
@@ -152,7 +150,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const processUsageData = useCallback(
|
||||
(data: SuccessResponse<UsageResponsePayloadProps> | ErrorResponse): void => {
|
||||
(data: any): void => {
|
||||
if (isEmpty(data?.payload)) {
|
||||
return;
|
||||
}
|
||||
@@ -160,23 +158,27 @@ export default function BillingContainer(): JSX.Element {
|
||||
details: { breakdown = [], billTotal },
|
||||
billingPeriodStart,
|
||||
billingPeriodEnd,
|
||||
} = (data as SuccessResponse<UsageResponsePayloadProps>).payload;
|
||||
const formattedUsageData: DataType[] = [];
|
||||
} = data?.payload || {};
|
||||
const formattedUsageData: any[] = [];
|
||||
|
||||
if (breakdown && Array.isArray(breakdown)) {
|
||||
for (let index = 0; index < breakdown.length; index += 1) {
|
||||
const element: BreakdownEntry = breakdown[index];
|
||||
const element = breakdown[index];
|
||||
|
||||
element?.tiers?.forEach((tier, i: number) => {
|
||||
formattedUsageData.push({
|
||||
key: `${index}${i}`,
|
||||
name: i === 0 ? element?.type : '',
|
||||
unit: element?.unit ?? '',
|
||||
dataIngested: `${tier.quantity} ${element?.unit}`,
|
||||
pricePerUnit: String(tier.unitPrice),
|
||||
cost: `$ ${tier.tierCost}`,
|
||||
});
|
||||
});
|
||||
element?.tiers.forEach(
|
||||
(
|
||||
tier: { quantity: number; unitPrice: number; tierCost: number },
|
||||
i: number,
|
||||
) => {
|
||||
formattedUsageData.push({
|
||||
key: `${index}${i}`,
|
||||
name: i === 0 ? element?.type : '',
|
||||
dataIngested: `${tier.quantity} ${element?.unit}`,
|
||||
pricePerUnit: tier.unitPrice,
|
||||
cost: `$ ${tier.tierCost}`,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,19 +251,16 @@ export default function BillingContainer(): JSX.Element {
|
||||
title: 'Data Ingested',
|
||||
dataIndex: 'dataIngested',
|
||||
key: 'dataIngested',
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: 'Price per Unit',
|
||||
dataIndex: 'pricePerUnit',
|
||||
key: 'pricePerUnit',
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: 'Cost',
|
||||
title: 'Cost (Billing period to date)',
|
||||
dataIndex: 'cost',
|
||||
key: 'cost',
|
||||
align: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -346,6 +345,23 @@ export default function BillingContainer(): JSX.Element {
|
||||
updateCreditCard,
|
||||
]);
|
||||
|
||||
const BillingUsageGraphCallback = useCallback(
|
||||
() =>
|
||||
!isLoading && !isFetchingBillingData ? (
|
||||
<>
|
||||
<BillingUsageGraph data={apiResponse} billAmount={billAmount} />
|
||||
<div className="billing-update-note">
|
||||
Note: Billing metrics are updated once every 24 hours.
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Card className="empty-graph-card" bordered={false}>
|
||||
<Spinner size="large" tip="Loading..." height="35vh" />
|
||||
</Card>
|
||||
),
|
||||
[apiResponse, billAmount, isLoading, isFetchingBillingData],
|
||||
);
|
||||
|
||||
const subscriptionPastDueMessage = (): JSX.Element => (
|
||||
<Typography>
|
||||
{`We were not able to process payments for your account. Please update your card details `}
|
||||
@@ -399,12 +415,12 @@ export default function BillingContainer(): JSX.Element {
|
||||
trialInfo?.gracePeriodEnd;
|
||||
|
||||
return (
|
||||
<div className={styles.billingContainer}>
|
||||
<Flex vertical gap={4} className={styles.pageHeader}>
|
||||
<Typography.Text className={styles.pageHeaderTitle}>
|
||||
<div className="billing-container">
|
||||
<Flex vertical style={{ marginBottom: 16 }}>
|
||||
<Typography.Text style={{ fontWeight: 500, fontSize: 18 }}>
|
||||
{t('billing')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.pageHeaderSubtitle}>
|
||||
<Typography.Text color="muted">
|
||||
{t('manage_billing_and_costs')}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
@@ -412,36 +428,50 @@ export default function BillingContainer(): JSX.Element {
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{ minHeight: 150, marginBottom: 16 }}
|
||||
className={styles.pageInfo}
|
||||
className="page-info"
|
||||
>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Flex vertical gap={8}>
|
||||
<p className={styles.pageInfoTitle}>
|
||||
<Flex vertical>
|
||||
<Typography.Title level={5} style={{ marginTop: 2, fontWeight: 500 }}>
|
||||
{isCloudUserVal ? t('teams_cloud') : t('teams')}{' '}
|
||||
{isFreeTrial ? <Badge color="success"> Free Trial </Badge> : ''}
|
||||
</p>
|
||||
</Typography.Title>
|
||||
|
||||
{!isLoading && !isFetchingBillingData && !showGracePeriodMessage ? (
|
||||
<p className={styles.pageInfoSubtitle}>
|
||||
<Typography.Text style={{ fontSize: 12, color: Color.BG_VANILLA_400 }}>
|
||||
{daysRemaining} {daysRemainingStr}
|
||||
</p>
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</Flex>
|
||||
<Button
|
||||
testId="header-billing-button"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="md"
|
||||
loading={isLoadingBilling || isLoadingManageBilling}
|
||||
disabled={isLoading}
|
||||
onClick={handleBilling}
|
||||
prefix={<Landmark size={14} />}
|
||||
className={styles.billingManageBtn}
|
||||
>
|
||||
{trialInfo?.trialConvertedToSubscription
|
||||
? t('manage_billing')
|
||||
: t('upgrade_plan')}
|
||||
</Button>
|
||||
<Flex gap={8}>
|
||||
<Button
|
||||
type="default"
|
||||
size="middle"
|
||||
loading={isLoadingBilling || isLoadingManageBilling}
|
||||
disabled={isLoading || isFetchingBillingData}
|
||||
onClick={handleCsvDownload}
|
||||
className="periscope-btn"
|
||||
>
|
||||
<Flex align="center" justify="center" gap={4}>
|
||||
<CloudDownload size="md" />
|
||||
Download CSV
|
||||
</Flex>
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="header-billing-button"
|
||||
type="primary"
|
||||
size="middle"
|
||||
loading={isLoadingBilling || isLoadingManageBilling}
|
||||
disabled={isLoading}
|
||||
onClick={handleBilling}
|
||||
>
|
||||
{trialInfo?.trialConvertedToSubscription
|
||||
? t('manage_billing')
|
||||
: t('upgrade_plan')}
|
||||
</Button>
|
||||
|
||||
<RefreshPaymentStatus type="tooltip" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{trialInfo?.onTrial && trialInfo?.trialConvertedToSubscription && (
|
||||
@@ -455,8 +485,8 @@ export default function BillingContainer(): JSX.Element {
|
||||
|
||||
{!isLoading && !isFetchingBillingData && !showGracePeriodMessage
|
||||
? headerText && (
|
||||
<Callout
|
||||
title={headerText}
|
||||
<Alert
|
||||
message={headerText}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginTop: 12 }}
|
||||
@@ -473,8 +503,8 @@ export default function BillingContainer(): JSX.Element {
|
||||
billingData &&
|
||||
trialInfo?.gracePeriodEnd &&
|
||||
showGracePeriodMessage ? (
|
||||
<Callout
|
||||
title={`Your data is safe with us until ${getFormattedDate(
|
||||
<Alert
|
||||
message={`Your data is safe with us until ${getFormattedDate(
|
||||
trialInfo?.gracePeriodEnd || Date.now(),
|
||||
)}. Please upgrade plan now to retain your data.`}
|
||||
type="info"
|
||||
@@ -485,69 +515,26 @@ export default function BillingContainer(): JSX.Element {
|
||||
|
||||
{isSubscriptionPastDue &&
|
||||
(!isLoading && !isFetchingBillingData ? (
|
||||
<Callout type="error" showIcon style={{ marginTop: 12 }}>
|
||||
{subscriptionPastDueMessage()}
|
||||
</Callout>
|
||||
<Alert
|
||||
message={subscriptionPastDueMessage()}
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginTop: 12 }}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton.Input active style={{ height: 20, marginTop: 20 }} />
|
||||
))}
|
||||
</Card>
|
||||
|
||||
<div className={styles.billingGraphSection}>
|
||||
{!isLoading && !isFetchingBillingData ? (
|
||||
<BillingUsageGraph data={apiResponse} billAmount={billAmount} />
|
||||
) : (
|
||||
<Card className={styles.emptyGraphCard} bordered={false}>
|
||||
<Spinner size="large" tip="Loading..." height="35vh" />
|
||||
</Card>
|
||||
)}
|
||||
{!isLoading && !isFetchingBillingData && (
|
||||
<div className={styles.billingGraphFooter}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="md"
|
||||
onClick={handleCsvDownload}
|
||||
prefix={<MonitorDown size={14} />}
|
||||
testId="download-csv-button"
|
||||
className={styles.billingFooterBtn}
|
||||
>
|
||||
Download CSV
|
||||
</Button>
|
||||
<RefreshPaymentStatus type="button" className={styles.billingFooterBtn} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isLoading && !isFetchingBillingData && (
|
||||
<Callout type="info" size="small" className={styles.billingUpdateNote}>
|
||||
Billing metrics are updated once every 24 hours.
|
||||
</Callout>
|
||||
)}
|
||||
<BillingUsageGraphCallback />
|
||||
|
||||
<div className={styles.billingDetails}>
|
||||
<div className="billing-details">
|
||||
{!isLoading && !isFetchingBillingData && (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
pagination={false}
|
||||
bordered={false}
|
||||
components={{
|
||||
header: {
|
||||
cell: ({
|
||||
style,
|
||||
...props
|
||||
}: React.ThHTMLAttributes<HTMLTableCellElement>): JSX.Element => {
|
||||
const { background: _, boxShadow: __, ...safeStyle } = style ?? {};
|
||||
return (
|
||||
<th
|
||||
{...props}
|
||||
style={safeStyle}
|
||||
className={`${props.className ?? ''} ${styles.billingDetailsHeaderCell}`}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -559,7 +546,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
)}
|
||||
|
||||
{!trialInfo?.trialConvertedToSubscription && (
|
||||
<div className={styles.upgradePlanBenefits}>
|
||||
<div className="upgrade-plan-benefits">
|
||||
<Row
|
||||
justify="space-between"
|
||||
align="middle"
|
||||
@@ -568,16 +555,16 @@ export default function BillingContainer(): JSX.Element {
|
||||
}}
|
||||
gutter={[16, 16]}
|
||||
>
|
||||
<Col span={20} className={styles.planBenefits}>
|
||||
<Typography.Text className={styles.planBenefit}>
|
||||
<Col span={20} className="plan-benefits">
|
||||
<Typography.Text className="plan-benefit">
|
||||
<CircleCheck size="md" />
|
||||
{t('upgrade_now_text')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.planBenefit}>
|
||||
<Typography.Text className="plan-benefit">
|
||||
<CircleCheck size="md" />
|
||||
{t('Your billing will start only after the trial period')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.planBenefit}>
|
||||
<Typography.Text className="plan-benefit">
|
||||
<CircleCheck size="md" />
|
||||
<span>
|
||||
{t('checkout_plans')}
|
||||
@@ -596,10 +583,9 @@ export default function BillingContainer(): JSX.Element {
|
||||
</Col>
|
||||
<Col span={4} style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
testId="upgrade-plan-button"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="md"
|
||||
data-testid="upgrade-plan-button"
|
||||
type="primary"
|
||||
size="middle"
|
||||
loading={isLoadingBilling || isLoadingManageBilling}
|
||||
onClick={handleBilling}
|
||||
>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.headerRow {
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.itemList {
|
||||
overflow-y: auto;
|
||||
max-height: 300px;
|
||||
padding: var(--padding-3);
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import cx from 'classnames';
|
||||
import TooltipHeader from 'lib/uPlotV2/components/Tooltip/components/TooltipHeader/TooltipHeader';
|
||||
import TooltipItem from 'lib/uPlotV2/components/Tooltip/components/TooltipItem/TooltipItem';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { getToolTipValue } from 'components/Graph/yAxisConfig';
|
||||
import { buildTooltipContent } from 'lib/uPlotV2/components/Tooltip/utils';
|
||||
import {
|
||||
TooltipContentItem,
|
||||
TooltipRenderArgs,
|
||||
} from 'lib/uPlotV2/components/types';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import TooltipStyles from 'lib/uPlotV2/components/Tooltip/Tooltip.module.scss';
|
||||
import Styles from './BillingBarChartTooltip.module.scss';
|
||||
|
||||
interface BillingBarChartTooltipProps extends TooltipRenderArgs {
|
||||
billingApiResponse: MetricRangePayloadProps;
|
||||
}
|
||||
|
||||
const CURRENCY_SYMBOL = '$';
|
||||
|
||||
export function BillingBarChartTooltip({
|
||||
billingApiResponse,
|
||||
uPlotInstance,
|
||||
dataIndexes,
|
||||
seriesIndex,
|
||||
isPinned,
|
||||
}: BillingBarChartTooltipProps): JSX.Element {
|
||||
const content = useMemo((): TooltipContentItem[] => {
|
||||
const baseItems = buildTooltipContent({
|
||||
data: uPlotInstance.data,
|
||||
series: uPlotInstance.series,
|
||||
dataIndexes,
|
||||
activeSeriesIndex: seriesIndex,
|
||||
uPlotInstance,
|
||||
yAxisUnit: '',
|
||||
isStackedBarChart: true,
|
||||
});
|
||||
|
||||
return baseItems.map((item) => {
|
||||
const match = billingApiResponse.data.result.find(
|
||||
(r) => (r.legend || r.queryName) === item.label,
|
||||
);
|
||||
|
||||
if (!match) {
|
||||
return item;
|
||||
}
|
||||
|
||||
const seriesIdx = uPlotInstance.series.findIndex(
|
||||
(s) => s.label === item.label,
|
||||
);
|
||||
if (seriesIdx === -1) {
|
||||
return item;
|
||||
}
|
||||
|
||||
const dataIndex = dataIndexes[seriesIdx];
|
||||
const quantity = dataIndex != null ? match.quantity?.[dataIndex] : null;
|
||||
const unit = match.unit ?? '';
|
||||
const quantityStr =
|
||||
quantity != null ? ` - ${getToolTipValue(quantity)} ${unit}` : '';
|
||||
|
||||
return {
|
||||
...item,
|
||||
tooltipValue: `${CURRENCY_SYMBOL}${getToolTipValue(item.value, '')}${quantityStr}`,
|
||||
};
|
||||
});
|
||||
}, [uPlotInstance, seriesIndex, dataIndexes, billingApiResponse]);
|
||||
|
||||
const activeItem = content.find((item) => item.isActive) ?? null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(TooltipStyles.container, {
|
||||
[TooltipStyles.pinned]: isPinned,
|
||||
})}
|
||||
data-testid="uplot-tooltip-container"
|
||||
>
|
||||
<TooltipHeader
|
||||
uPlotInstance={uPlotInstance}
|
||||
showTooltipHeader
|
||||
isPinned={isPinned}
|
||||
activeItem={null}
|
||||
headerRowClassName={Styles.headerRow}
|
||||
dateFormat={DATE_TIME_FORMATS.MONTH_DATE}
|
||||
/>
|
||||
{activeItem != null && <span className={TooltipStyles.divider} />}
|
||||
<div className={Styles.itemList} data-testid="uplot-tooltip-list">
|
||||
{content.map((item) => (
|
||||
<TooltipItem key={item.label} item={item} isItemActive={item.isActive} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
.graphContainer {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.billingGraphCard {
|
||||
:global {
|
||||
.uplot-no-data {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
height: 40vh;
|
||||
|
||||
.uplot-graph-container {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.totalSpent {
|
||||
font-family: 'SF Mono', monospace;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.totalSpentTitle {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.48px;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
.billing-graph-card {
|
||||
.ant-card-body {
|
||||
height: 40vh;
|
||||
.uplot-graph-container {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
.total-spent {
|
||||
font-family: 'SF Mono' monospace;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.total-spent-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.48px;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
@@ -1,146 +1,221 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Card, Flex } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
import {
|
||||
LegendPosition,
|
||||
TooltipRenderArgs,
|
||||
} from 'lib/uPlotV2/components/types';
|
||||
import type { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import type uPlot from 'uplot';
|
||||
import type { UsageResponsePayloadProps } from 'api/billing/getUsage';
|
||||
import tooltipPlugin from 'lib/uPlotLib/plugins/tooltipPlugin';
|
||||
import getAxes from 'lib/uPlotLib/utils/getAxes';
|
||||
import getRenderer from 'lib/uPlotLib/utils/getRenderer';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { getXAxisScale } from 'lib/uPlotLib/utils/getXAxisScale';
|
||||
import { getYAxisScale } from 'lib/uPlotLib/utils/getYAxisScale';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { BillingBarChartTooltip } from './BillingBarChartTooltip';
|
||||
import { prepareBillingBarConfig } from './prepareBillingBarConfig';
|
||||
import {
|
||||
calculateStartEndTime,
|
||||
convertDataToMetricRangePayload,
|
||||
fillMissingValuesForQuantities,
|
||||
} from './utils';
|
||||
|
||||
import styles from './BillingUsageGraph.module.scss';
|
||||
import './BillingUsageGraph.styles.scss';
|
||||
import '../../../lib/uPlotLib/uPlotLib.styles.scss';
|
||||
|
||||
interface BillingUsageGraphProps {
|
||||
data: Partial<UsageResponsePayloadProps>;
|
||||
data: any;
|
||||
billAmount: number;
|
||||
}
|
||||
const paths = (
|
||||
u: any,
|
||||
seriesIdx: number,
|
||||
idx0: number,
|
||||
idx1: number,
|
||||
extendGap: boolean,
|
||||
buildClip: boolean,
|
||||
): uPlot.Series.PathBuilder => {
|
||||
const s = u.series[seriesIdx];
|
||||
const style = s.drawStyle;
|
||||
const interp = s.lineInterpolation;
|
||||
|
||||
const numberFormatter = new Intl.NumberFormat('en-US');
|
||||
const renderer = getRenderer(style, interp);
|
||||
|
||||
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
|
||||
};
|
||||
|
||||
const calculateStartEndTime = (
|
||||
data: any,
|
||||
): { startTime: number; endTime: number } => {
|
||||
const timestamps: number[] = [];
|
||||
data?.details?.breakdown?.forEach((breakdown: any) => {
|
||||
breakdown?.dayWiseBreakdown?.breakdown?.forEach((entry: any) => {
|
||||
timestamps.push(entry?.timestamp);
|
||||
});
|
||||
});
|
||||
const billingTime = [data?.billingPeriodStart, data?.billingPeriodEnd];
|
||||
const startTime: number = Math.min(...timestamps, ...billingTime);
|
||||
const endTime: number = Math.max(...timestamps, ...billingTime);
|
||||
return { startTime, endTime };
|
||||
};
|
||||
|
||||
export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
|
||||
const { data, billAmount } = props;
|
||||
|
||||
// Added this to fix the issue where breakdown with one day data are causing the bars to spread across multiple days
|
||||
data?.details?.breakdown?.forEach((breakdown: any) => {
|
||||
if (breakdown?.dayWiseBreakdown?.breakdown?.length === 1) {
|
||||
const currentDay = breakdown.dayWiseBreakdown.breakdown[0];
|
||||
const nextDay = {
|
||||
...currentDay,
|
||||
timestamp: currentDay.timestamp + 86400,
|
||||
count: 0,
|
||||
size: 0,
|
||||
quantity: 0,
|
||||
total: 0,
|
||||
};
|
||||
breakdown.dayWiseBreakdown.breakdown.push(nextDay);
|
||||
}
|
||||
});
|
||||
const graphCompatibleData = useMemo(
|
||||
() => convertDataToMetricRangePayload(data),
|
||||
[data],
|
||||
);
|
||||
const chartData = getUPlotChartData(graphCompatibleData);
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
// Single-day data causes bars to span multiple days — add a synthetic
|
||||
// zero-value next-day entry so uPlot renders a correctly-sized single-day bar.
|
||||
const normalizedData = useMemo(() => {
|
||||
if (!data?.details?.breakdown) {
|
||||
return data;
|
||||
}
|
||||
return {
|
||||
...data,
|
||||
details: {
|
||||
...data.details,
|
||||
breakdown: data.details.breakdown.map((breakdown) => {
|
||||
if (breakdown?.dayWiseBreakdown?.breakdown?.length !== 1) {
|
||||
return breakdown;
|
||||
}
|
||||
const currentDay = breakdown.dayWiseBreakdown.breakdown[0];
|
||||
const nextDay = {
|
||||
...currentDay,
|
||||
timestamp: currentDay.timestamp + 86400,
|
||||
count: 0,
|
||||
size: 0,
|
||||
quantity: 0,
|
||||
total: 0,
|
||||
};
|
||||
return {
|
||||
...breakdown,
|
||||
dayWiseBreakdown: {
|
||||
...breakdown.dayWiseBreakdown,
|
||||
breakdown: [...breakdown.dayWiseBreakdown.breakdown, nextDay],
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
const graphCompatibleData = useMemo(
|
||||
() => convertDataToMetricRangePayload(normalizedData),
|
||||
[normalizedData],
|
||||
);
|
||||
|
||||
const chartData = useMemo(
|
||||
() => prepareChartData(graphCompatibleData) as uPlot.AlignedData,
|
||||
[graphCompatibleData],
|
||||
);
|
||||
|
||||
const filledApiResponse = useMemo(
|
||||
(): MetricRangePayloadProps =>
|
||||
fillMissingValuesForQuantities(
|
||||
graphCompatibleData,
|
||||
chartData[0] as number[],
|
||||
),
|
||||
[graphCompatibleData, chartData],
|
||||
);
|
||||
|
||||
const { startTime, endTime } = useMemo(
|
||||
() =>
|
||||
calculateStartEndTime(normalizedData as Partial<UsageResponsePayloadProps>),
|
||||
[normalizedData],
|
||||
() => calculateStartEndTime(data),
|
||||
[data],
|
||||
);
|
||||
|
||||
const config = useMemo(
|
||||
() =>
|
||||
prepareBillingBarConfig({
|
||||
isDarkMode,
|
||||
// Subtract 86400s (one day) from startTime to add a buffer before first bar
|
||||
minTimeScale: startTime !== undefined ? startTime - 86400 : undefined,
|
||||
maxTimeScale: endTime,
|
||||
apiResponse: graphCompatibleData,
|
||||
}),
|
||||
[isDarkMode, startTime, endTime, graphCompatibleData],
|
||||
const getGraphSeries = (color: string, label: string): any => ({
|
||||
drawStyle: 'bars',
|
||||
paths,
|
||||
lineInterpolation: 'spline',
|
||||
show: true,
|
||||
label,
|
||||
fill: color,
|
||||
stroke: color,
|
||||
width: 2,
|
||||
spanGaps: true,
|
||||
points: {
|
||||
size: 5,
|
||||
show: false,
|
||||
stroke: color,
|
||||
},
|
||||
});
|
||||
|
||||
const uPlotSeries: any = useMemo(
|
||||
() => [
|
||||
{ label: 'Timestamp', stroke: 'purple' },
|
||||
getGraphSeries(
|
||||
'#7CEDBE',
|
||||
graphCompatibleData.data.result[0]?.legend as string,
|
||||
),
|
||||
getGraphSeries(
|
||||
'#4E74F8',
|
||||
graphCompatibleData.data.result[1]?.legend as string,
|
||||
),
|
||||
getGraphSeries(
|
||||
'#F24769',
|
||||
graphCompatibleData.data.result[2]?.legend as string,
|
||||
),
|
||||
],
|
||||
[graphCompatibleData.data.result],
|
||||
);
|
||||
|
||||
const renderBillingTooltip = useCallback(
|
||||
(args: TooltipRenderArgs) => (
|
||||
<BillingBarChartTooltip billingApiResponse={filledApiResponse} {...args} />
|
||||
),
|
||||
[filledApiResponse],
|
||||
const axesOptions = getAxes({ isDarkMode, yAxisUnit: '' });
|
||||
|
||||
const optionsForChart: uPlot.Options = useMemo(
|
||||
() => ({
|
||||
id: 'billing-usage-breakdown',
|
||||
series: uPlotSeries,
|
||||
width: containerDimensions.width,
|
||||
height: containerDimensions.height - 30,
|
||||
axes: [
|
||||
{
|
||||
...axesOptions[0],
|
||||
grid: {
|
||||
...axesOptions.grid,
|
||||
show: false,
|
||||
stroke: isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400,
|
||||
},
|
||||
},
|
||||
{
|
||||
...axesOptions[1],
|
||||
stroke: isDarkMode ? Color.BG_SLATE_200 : Color.BG_INK_400,
|
||||
},
|
||||
],
|
||||
scales: {
|
||||
x: {
|
||||
...getXAxisScale(startTime - 86400, endTime), // Minus 86400 from startTime to decrease a day to have a buffer start
|
||||
},
|
||||
y: {
|
||||
...getYAxisScale({
|
||||
series: graphCompatibleData?.data?.newResult?.data?.result,
|
||||
yAxisUnit: '',
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
}),
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
live: false,
|
||||
isolate: true,
|
||||
},
|
||||
cursor: {
|
||||
lock: false,
|
||||
focus: {
|
||||
prox: 1e6,
|
||||
bias: 1,
|
||||
},
|
||||
},
|
||||
focus: {
|
||||
alpha: 0.3,
|
||||
},
|
||||
padding: [32, 32, 16, 16],
|
||||
plugins: [
|
||||
tooltipPlugin({
|
||||
apiResponse: fillMissingValuesForQuantities(
|
||||
graphCompatibleData,
|
||||
chartData[0],
|
||||
),
|
||||
yAxisUnit: '',
|
||||
isBillingUsageGraphs: true,
|
||||
isDarkMode,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
[
|
||||
axesOptions,
|
||||
chartData,
|
||||
containerDimensions.height,
|
||||
containerDimensions.width,
|
||||
endTime,
|
||||
graphCompatibleData,
|
||||
isDarkMode,
|
||||
startTime,
|
||||
uPlotSeries,
|
||||
],
|
||||
);
|
||||
|
||||
const numberFormatter = new Intl.NumberFormat('en-US');
|
||||
|
||||
return (
|
||||
<Card bordered={false} className={styles.billingGraphCard}>
|
||||
<Card bordered={false} className="billing-graph-card">
|
||||
<Flex justify="space-between">
|
||||
<Flex vertical gap={6}>
|
||||
<Typography.Text className={styles.totalSpentTitle}>
|
||||
<Typography.Text className="total-spent-title">
|
||||
TOTAL SPENT
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.totalSpent}>
|
||||
<Typography.Text className="total-spent">
|
||||
${numberFormatter.format(billAmount)}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<div ref={graphRef} className={styles.graphContainer}>
|
||||
{containerDimensions.width > 0 && containerDimensions.height > 0 && (
|
||||
<BarChart
|
||||
config={config}
|
||||
data={chartData}
|
||||
isStackedBarChart
|
||||
legendConfig={{ position: LegendPosition.BOTTOM }}
|
||||
customTooltip={renderBillingTooltip}
|
||||
width={containerDimensions.width}
|
||||
height={containerDimensions.height - 30}
|
||||
canPinTooltip
|
||||
/>
|
||||
)}
|
||||
<div ref={graphRef} style={{ height: '100%', paddingBottom: 48 }}>
|
||||
<Uplot data={chartData} options={optionsForChart} />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { BillingBarChartTooltip } from '../BillingBarChartTooltip';
|
||||
|
||||
// Mock buildTooltipContent so tests don't depend on uPlot stacking math
|
||||
jest.mock('lib/uPlotV2/components/Tooltip/utils', () => ({
|
||||
buildTooltipContent: jest.fn().mockReturnValue([
|
||||
{
|
||||
label: 'Logs',
|
||||
value: 100,
|
||||
tooltipValue: '$100.00',
|
||||
color: '#7CEDBE',
|
||||
isActive: true,
|
||||
isHighlighted: false,
|
||||
},
|
||||
{
|
||||
label: 'Traces',
|
||||
value: 50,
|
||||
tooltipValue: '$50.00',
|
||||
color: '#4E74F8',
|
||||
isActive: false,
|
||||
isHighlighted: false,
|
||||
},
|
||||
]),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
function makeUPlotInstance(seriesLabels: string[]): uPlot {
|
||||
return {
|
||||
data: [
|
||||
[1000, 2000],
|
||||
[100, 200],
|
||||
[50, 80],
|
||||
],
|
||||
cursor: { idx: 0 },
|
||||
series: [
|
||||
{ label: 'Timestamp', show: true, stroke: '#000' },
|
||||
...seriesLabels.map((label) => ({
|
||||
label,
|
||||
show: true,
|
||||
stroke: '#aabbcc',
|
||||
})),
|
||||
],
|
||||
} as unknown as uPlot;
|
||||
}
|
||||
|
||||
function makeBillingApiResponse(
|
||||
entries: { legend: string; quantity: (number | null)[]; unit: string }[],
|
||||
): MetricRangePayloadProps {
|
||||
return {
|
||||
data: {
|
||||
result: entries.map((e) => ({
|
||||
legend: e.legend,
|
||||
queryName: e.legend,
|
||||
metric: {},
|
||||
values: [[1000, '10']] as [number, string][],
|
||||
quantity: e.quantity as number[],
|
||||
unit: e.unit,
|
||||
})),
|
||||
resultType: '',
|
||||
newResult: { data: { result: [], resultType: '' } },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const baseTooltipArgs = {
|
||||
isPinned: false,
|
||||
dismiss: jest.fn(),
|
||||
viaSync: false,
|
||||
seriesIndex: 1,
|
||||
dataIndexes: [null, 0, 0],
|
||||
};
|
||||
|
||||
describe('BillingBarChartTooltip', () => {
|
||||
it('augments tooltipValue with quantity and unit for each series', () => {
|
||||
const uPlotInstance = makeUPlotInstance(['Logs', 'Traces']);
|
||||
const billingApiResponse = makeBillingApiResponse([
|
||||
{ legend: 'Logs', quantity: [1.5, 2.0], unit: 'GB' },
|
||||
{ legend: 'Traces', quantity: [500, 800], unit: 'spans' },
|
||||
]);
|
||||
|
||||
render(
|
||||
<BillingBarChartTooltip
|
||||
{...baseTooltipArgs}
|
||||
uPlotInstance={uPlotInstance}
|
||||
billingApiResponse={billingApiResponse}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getAllByText(/1\.5 GB/i).length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText(/500 spans/i).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('omits quantity line when quantity at dataIndex is null', () => {
|
||||
const uPlotInstance = makeUPlotInstance(['Logs', 'Traces']);
|
||||
const billingApiResponse = makeBillingApiResponse([
|
||||
{ legend: 'Logs', quantity: [null, null], unit: 'GB' },
|
||||
{ legend: 'Traces', quantity: [null, null], unit: 'spans' },
|
||||
]);
|
||||
|
||||
render(
|
||||
<BillingBarChartTooltip
|
||||
{...baseTooltipArgs}
|
||||
uPlotInstance={uPlotInstance}
|
||||
billingApiResponse={billingApiResponse}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/null GB/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/null spans/i)).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('uplot-tooltip-container')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('formats dollar value via getToolTipValue — strips trailing zeros (0.3076 → $0.3)', () => {
|
||||
const uPlotInstance = makeUPlotInstance(['Logs']);
|
||||
const { buildTooltipContent } = jest.requireMock(
|
||||
'lib/uPlotV2/components/Tooltip/utils',
|
||||
) as { buildTooltipContent: jest.Mock };
|
||||
buildTooltipContent.mockReturnValueOnce([
|
||||
{
|
||||
label: 'Logs',
|
||||
value: 0.3076171875,
|
||||
tooltipValue: '$0.31',
|
||||
color: '#7CEDBE',
|
||||
isActive: true,
|
||||
isHighlighted: false,
|
||||
},
|
||||
]);
|
||||
const billingApiResponse = makeBillingApiResponse([
|
||||
{ legend: 'Logs', quantity: [1.23], unit: 'GB' },
|
||||
]);
|
||||
|
||||
render(
|
||||
<BillingBarChartTooltip
|
||||
{...baseTooltipArgs}
|
||||
uPlotInstance={uPlotInstance}
|
||||
billingApiResponse={billingApiResponse}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getAllByText(/\$0\.3 -/i).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('passes through base tooltipValue when series is not in billingApiResponse', () => {
|
||||
const uPlotInstance = makeUPlotInstance(['Logs', 'Traces']);
|
||||
const billingApiResponse = makeBillingApiResponse([]);
|
||||
|
||||
render(
|
||||
<BillingBarChartTooltip
|
||||
{...baseTooltipArgs}
|
||||
uPlotInstance={uPlotInstance}
|
||||
billingApiResponse={billingApiResponse}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getAllByText('$100.00').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('$50.00').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { prepareBillingBarConfig } from '../prepareBillingBarConfig';
|
||||
|
||||
const makeApiResponse = (legends: string[]): MetricRangePayloadProps => ({
|
||||
data: {
|
||||
result: legends.map((legend) => ({
|
||||
legend,
|
||||
queryName: legend,
|
||||
metric: {},
|
||||
values: [[1000, '10']],
|
||||
})),
|
||||
resultType: '',
|
||||
newResult: { data: { result: [], resultType: '' } },
|
||||
},
|
||||
});
|
||||
|
||||
describe('prepareBillingBarConfig', () => {
|
||||
const baseProps = { isDarkMode: false };
|
||||
|
||||
it('returns a builder with no series when apiResponse is undefined', () => {
|
||||
const builder = prepareBillingBarConfig(baseProps);
|
||||
const config = builder.getConfig();
|
||||
expect(config.series).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns a builder with no series when result is empty', () => {
|
||||
const builder = prepareBillingBarConfig({
|
||||
...baseProps,
|
||||
apiResponse: makeApiResponse([]),
|
||||
});
|
||||
const config = builder.getConfig();
|
||||
expect(config.series).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('adds one series per result entry with correct labels and colors', () => {
|
||||
const builder = prepareBillingBarConfig({
|
||||
...baseProps,
|
||||
apiResponse: makeApiResponse(['Logs', 'Traces', 'Metrics']),
|
||||
});
|
||||
const config = builder.getConfig();
|
||||
expect(config.series).toHaveLength(4);
|
||||
expect(config.series?.[1]?.label).toBe('Logs');
|
||||
expect(config.series?.[1]?.stroke).toBe(Color.BG_FOREST_300);
|
||||
expect(config.series?.[2]?.label).toBe('Traces');
|
||||
expect(config.series?.[2]?.stroke).toBe(Color.BG_ROBIN_500);
|
||||
expect(config.series?.[3]?.label).toBe('Metrics');
|
||||
expect(config.series?.[3]?.stroke).toBe(Color.BG_SAKURA_500);
|
||||
});
|
||||
|
||||
it('assigns fallback color (Amber500) for signals beyond the 3-color palette', () => {
|
||||
const builder = prepareBillingBarConfig({
|
||||
...baseProps,
|
||||
apiResponse: makeApiResponse(['A', 'B', 'C', 'D']),
|
||||
});
|
||||
const config = builder.getConfig();
|
||||
expect(config.series?.[4]?.stroke).toBe(Color.BG_AMBER_500);
|
||||
});
|
||||
|
||||
it('sets stacking bands, padding, and focus alpha for behavioral parity', () => {
|
||||
const builder = prepareBillingBarConfig({
|
||||
...baseProps,
|
||||
apiResponse: makeApiResponse(['Logs', 'Traces', 'Metrics']),
|
||||
});
|
||||
const config = builder.getConfig();
|
||||
expect(config.bands).toStrictEqual([{ series: [1, 2] }, { series: [2, 3] }]);
|
||||
expect(config.padding).toStrictEqual([32, 32, 16, 16]);
|
||||
expect(config.focus).toStrictEqual({ alpha: 0.3 });
|
||||
});
|
||||
|
||||
it('sets no bands when result is empty', () => {
|
||||
const builder = prepareBillingBarConfig({
|
||||
...baseProps,
|
||||
apiResponse: makeApiResponse([]),
|
||||
});
|
||||
const config = builder.getConfig();
|
||||
expect(config.bands).toBeUndefined();
|
||||
});
|
||||
|
||||
it('uses queryName as label when legend is undefined', () => {
|
||||
const apiResponse: MetricRangePayloadProps = {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
legend: undefined as any,
|
||||
queryName: 'Logs',
|
||||
metric: {},
|
||||
values: [[1000, '10']],
|
||||
},
|
||||
],
|
||||
resultType: '',
|
||||
newResult: { data: { result: [], resultType: '' } },
|
||||
},
|
||||
};
|
||||
const builder = prepareBillingBarConfig({ isDarkMode: false, apiResponse });
|
||||
const config = builder.getConfig();
|
||||
expect(config.series?.[1]?.label).toBe('Logs');
|
||||
expect(config.series?.[1]?.stroke).toBe(Color.BG_FOREST_300);
|
||||
});
|
||||
});
|
||||
@@ -1,145 +0,0 @@
|
||||
import {
|
||||
calculateStartEndTime,
|
||||
convertDataToMetricRangePayload,
|
||||
} from '../utils';
|
||||
|
||||
const makeData = (
|
||||
timestamps: number[],
|
||||
billingPeriodStart?: number,
|
||||
billingPeriodEnd?: number,
|
||||
) => ({
|
||||
billingPeriodStart,
|
||||
billingPeriodEnd,
|
||||
details: {
|
||||
total: 0,
|
||||
baseFee: 0,
|
||||
billTotal: 0,
|
||||
breakdown: [
|
||||
{
|
||||
type: 'Logs',
|
||||
unit: 'GB',
|
||||
dayWiseBreakdown: {
|
||||
breakdown: timestamps.map((timestamp) => ({
|
||||
timestamp,
|
||||
total: 0,
|
||||
quantity: 0,
|
||||
count: 0,
|
||||
size: 0,
|
||||
})),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
describe('convertDataToMetricRangePayload', () => {
|
||||
it('returns empty result when all dayWiseBreakdown.breakdown are null', () => {
|
||||
const data = {
|
||||
billingPeriodStart: 1778763678,
|
||||
billingPeriodEnd: 1781442078,
|
||||
details: {
|
||||
total: 0,
|
||||
baseFee: 49,
|
||||
billTotal: 49,
|
||||
breakdown: [
|
||||
{
|
||||
type: 'Metrics',
|
||||
unit: 'Million',
|
||||
tiers: [],
|
||||
dayWiseBreakdown: { type: '', breakdown: null },
|
||||
},
|
||||
{
|
||||
type: 'Traces',
|
||||
unit: 'GB',
|
||||
tiers: [],
|
||||
dayWiseBreakdown: { type: '', breakdown: null },
|
||||
},
|
||||
{
|
||||
type: 'Logs',
|
||||
unit: 'GB',
|
||||
tiers: [],
|
||||
dayWiseBreakdown: { type: '', breakdown: null },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const result = convertDataToMetricRangePayload(data);
|
||||
expect(result.data.result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('includes only series that have day-wise data', () => {
|
||||
const data = {
|
||||
details: {
|
||||
breakdown: [
|
||||
{
|
||||
type: 'Metrics',
|
||||
unit: 'Million',
|
||||
dayWiseBreakdown: { breakdown: null },
|
||||
},
|
||||
{
|
||||
type: 'Logs',
|
||||
unit: 'GB',
|
||||
dayWiseBreakdown: {
|
||||
breakdown: [
|
||||
{ timestamp: 1000, total: 5, quantity: 10, count: 0, size: 0 },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const result = convertDataToMetricRangePayload(data);
|
||||
expect(result.data.result).toHaveLength(1);
|
||||
expect(result.data.result[0].legend).toBe('Logs');
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateStartEndTime', () => {
|
||||
it('returns min/max of all breakdown timestamps', () => {
|
||||
const data = makeData([1000, 3000, 2000]);
|
||||
expect(calculateStartEndTime(data)).toStrictEqual({
|
||||
startTime: 1000,
|
||||
endTime: 3000,
|
||||
});
|
||||
});
|
||||
|
||||
it('includes billingPeriodStart and billingPeriodEnd in the range', () => {
|
||||
const data = makeData([2000, 3000], 500, 4000);
|
||||
expect(calculateStartEndTime(data)).toStrictEqual({
|
||||
startTime: 500,
|
||||
endTime: 4000,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns undefined when there are no timestamps and no billing period', () => {
|
||||
expect(calculateStartEndTime({})).toStrictEqual({
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns undefined when breakdown is empty', () => {
|
||||
const data = makeData([]);
|
||||
expect(calculateStartEndTime(data)).toStrictEqual({
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('filters out non-finite billingPeriod values', () => {
|
||||
const data = makeData([1000], NaN, Infinity);
|
||||
expect(calculateStartEndTime(data)).toStrictEqual({
|
||||
startTime: 1000,
|
||||
endTime: 1000,
|
||||
});
|
||||
});
|
||||
|
||||
it('works when details is missing', () => {
|
||||
expect(
|
||||
calculateStartEndTime({ billingPeriodStart: 100, billingPeriodEnd: 200 }),
|
||||
).toStrictEqual({
|
||||
startTime: 100,
|
||||
endTime: 200,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import type { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getInitialStackedBands } from 'container/DashboardContainer/visualization/charts/utils/stackSeriesUtils';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { DrawStyle } from 'lib/uPlotV2/config/types';
|
||||
import type { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import type { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
const BILLING_SERIES_COLORS = [
|
||||
Color.BG_FOREST_300,
|
||||
Color.BG_ROBIN_500,
|
||||
Color.BG_SAKURA_500,
|
||||
];
|
||||
|
||||
export interface PrepareBillingBarConfigProps {
|
||||
isDarkMode: boolean;
|
||||
timezone?: Timezone;
|
||||
minTimeScale?: number;
|
||||
maxTimeScale?: number;
|
||||
apiResponse?: MetricRangePayloadProps;
|
||||
}
|
||||
|
||||
export function prepareBillingBarConfig({
|
||||
isDarkMode,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
apiResponse,
|
||||
}: PrepareBillingBarConfigProps): UPlotConfigBuilder {
|
||||
const builder = buildBaseConfig({
|
||||
id: 'billing-usage-breakdown',
|
||||
isDarkMode,
|
||||
timezone,
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
});
|
||||
|
||||
const results = apiResponse?.data?.result;
|
||||
if (!results?.length) {
|
||||
return builder;
|
||||
}
|
||||
|
||||
const labels = results.map((s) => s.legend || s.queryName || '');
|
||||
|
||||
const colorMapping = labels.reduce<Record<string, string>>(
|
||||
(acc, label, index) => {
|
||||
acc[label] = BILLING_SERIES_COLORS[index] ?? Color.BG_AMBER_500;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
labels.forEach((label) => {
|
||||
builder.addSeries({
|
||||
scaleKey: 'y',
|
||||
drawStyle: DrawStyle.Bar,
|
||||
label,
|
||||
colorMapping,
|
||||
isDarkMode,
|
||||
metric: {},
|
||||
});
|
||||
});
|
||||
|
||||
builder.setBands(getInitialStackedBands(results.length));
|
||||
builder.setPadding([32, 32, 16, 16]);
|
||||
builder.setFocus({ alpha: 0.3 });
|
||||
|
||||
return builder;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UsageResponsePayloadProps } from 'api/billing/getUsage';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import dayjs from 'dayjs';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { isEmpty, isNull } from 'lodash-es';
|
||||
import { unparse } from 'papaparse';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
@@ -29,25 +29,23 @@ export const convertDataToMetricRangePayload = (
|
||||
return emptyStateData;
|
||||
}
|
||||
|
||||
const payload = breakdown
|
||||
.map((info: any) => {
|
||||
const metric = info.type;
|
||||
const sortedBreakdownData = (info?.dayWiseBreakdown?.breakdown || []).sort(
|
||||
(a: any, b: any) => a.timestamp - b.timestamp,
|
||||
);
|
||||
const values = (sortedBreakdownData || []).map((categoryInfo: any) => [
|
||||
categoryInfo.timestamp,
|
||||
categoryInfo.total,
|
||||
]);
|
||||
const queryName = info.type;
|
||||
const legend = info.type;
|
||||
const { unit } = info;
|
||||
const quantity = sortedBreakdownData.map(
|
||||
(categoryInfo: any) => categoryInfo.quantity,
|
||||
);
|
||||
return { metric, values, queryName, legend, quantity, unit };
|
||||
})
|
||||
.filter((series: any) => series.values.length > 0);
|
||||
const payload = breakdown.map((info: any) => {
|
||||
const metric = info.type;
|
||||
const sortedBreakdownData = (info?.dayWiseBreakdown?.breakdown || []).sort(
|
||||
(a: any, b: any) => a.timestamp - b.timestamp,
|
||||
);
|
||||
const values = (sortedBreakdownData || []).map((categoryInfo: any) => [
|
||||
categoryInfo.timestamp,
|
||||
categoryInfo.total,
|
||||
]);
|
||||
const queryName = info.type;
|
||||
const legend = info.type;
|
||||
const { unit } = info;
|
||||
const quantity = sortedBreakdownData.map(
|
||||
(categoryInfo: any) => categoryInfo.quantity,
|
||||
);
|
||||
return { metric, values, queryName, legend, quantity, unit };
|
||||
});
|
||||
|
||||
const sortedData = payload.sort((a: any, b: any) => {
|
||||
const sumA = a.values.reduce((acc: any, val: any) => acc + val[1], 0);
|
||||
@@ -122,40 +120,11 @@ export function prepareCsvData(data: Partial<UsageResponsePayloadProps>): {
|
||||
fileName: string;
|
||||
} {
|
||||
const graphCompatibleData = convertDataToMetricRangePayload(data);
|
||||
const chartData = prepareChartData(graphCompatibleData);
|
||||
const quantityMapArr = quantityDataArr(
|
||||
graphCompatibleData,
|
||||
chartData[0] as number[],
|
||||
);
|
||||
const chartData = getUPlotChartData(graphCompatibleData);
|
||||
const quantityMapArr = quantityDataArr(graphCompatibleData, chartData[0]);
|
||||
|
||||
return {
|
||||
csvData: unparse(generateCsvData(quantityMapArr)),
|
||||
fileName: csvFileName(quantityMapArr),
|
||||
};
|
||||
}
|
||||
|
||||
export function calculateStartEndTime(
|
||||
data: Partial<UsageResponsePayloadProps>,
|
||||
): { startTime: number | undefined; endTime: number | undefined } {
|
||||
const timestamps: number[] = [];
|
||||
data?.details?.breakdown?.forEach((breakdown) => {
|
||||
breakdown?.dayWiseBreakdown?.breakdown?.forEach((entry) => {
|
||||
timestamps.push(entry.timestamp);
|
||||
});
|
||||
});
|
||||
|
||||
const billingTime: number[] = [
|
||||
data?.billingPeriodStart,
|
||||
data?.billingPeriodEnd,
|
||||
].filter((t): t is number => typeof t === 'number' && Number.isFinite(t));
|
||||
|
||||
const allTimes = [...timestamps, ...billingTime];
|
||||
if (allTimes.length === 0) {
|
||||
return { startTime: undefined, endTime: undefined };
|
||||
}
|
||||
|
||||
return {
|
||||
startTime: Math.min(...allTimes),
|
||||
endTime: Math.max(...allTimes),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,9 +9,8 @@ import { Skeleton } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
getListAccountServicesMetadataQueryKey,
|
||||
invalidateGetAccountService,
|
||||
invalidateGetService,
|
||||
invalidateListAccountServicesMetadata,
|
||||
useGetAccountService,
|
||||
useGetService,
|
||||
useUpdateService,
|
||||
} from 'api/generated/services/cloudintegration';
|
||||
@@ -119,50 +118,30 @@ function ServiceDetails({
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId');
|
||||
const serviceId = urlQuery.get('service');
|
||||
const isReadOnly = !cloudAccountId;
|
||||
const serviceQueryParams = cloudAccountId
|
||||
? { cloud_integration_id: cloudAccountId }
|
||||
: undefined;
|
||||
|
||||
const {
|
||||
queryKey: _accountServiceQueryKey,
|
||||
data: accountServiceData,
|
||||
isLoading: isAccountServiceLoading,
|
||||
} = useGetAccountService(
|
||||
{
|
||||
cloudProvider: type,
|
||||
id: cloudAccountId || '',
|
||||
serviceId: serviceId || '',
|
||||
},
|
||||
{
|
||||
query: {
|
||||
enabled: !!serviceId && !!cloudAccountId,
|
||||
select: (response): ServiceDetailsData => response.data,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
queryKey: _readOnlyServiceQueryKey,
|
||||
data: readOnlyServiceData,
|
||||
isLoading: isReadOnlyServiceLoading,
|
||||
queryKey: _queryKey,
|
||||
data: serviceDetailsData,
|
||||
isLoading: isServiceDetailsLoading,
|
||||
} = useGetService(
|
||||
{
|
||||
cloudProvider: type,
|
||||
serviceId: serviceId || '',
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
...serviceQueryParams,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
enabled: !!serviceId && !cloudAccountId,
|
||||
enabled: !!serviceId,
|
||||
select: (response): ServiceDetailsData => response.data,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const serviceDetailsData = cloudAccountId
|
||||
? accountServiceData
|
||||
: readOnlyServiceData;
|
||||
const isServiceDetailsLoading = cloudAccountId
|
||||
? isAccountServiceLoading
|
||||
: isReadOnlyServiceLoading;
|
||||
|
||||
const integrationConfig =
|
||||
type === IntegrationType.AWS_SERVICES
|
||||
? serviceDetailsData?.cloudIntegrationService?.config?.aws
|
||||
@@ -289,11 +268,16 @@ function ServiceDetails({
|
||||
},
|
||||
);
|
||||
|
||||
invalidateGetAccountService(queryClient, {
|
||||
cloudProvider: type,
|
||||
id: cloudAccountId,
|
||||
serviceId,
|
||||
});
|
||||
invalidateGetService(
|
||||
queryClient,
|
||||
{
|
||||
cloudProvider: type,
|
||||
serviceId,
|
||||
},
|
||||
{
|
||||
cloud_integration_id: cloudAccountId,
|
||||
},
|
||||
);
|
||||
|
||||
invalidateListAccountServicesMetadata(queryClient, {
|
||||
cloudProvider: type,
|
||||
|
||||
@@ -64,7 +64,7 @@ describe('ServiceDetails for S3 Sync service', () => {
|
||||
(_req, res, ctx) => res(ctx.json(accountsResponse)),
|
||||
),
|
||||
rest.get(
|
||||
'http://localhost/api/v1/cloud_integrations/aws/accounts/:accountId/services/:serviceId',
|
||||
'http://localhost/api/v1/cloud_integrations/aws/services/:serviceId',
|
||||
(req, res, ctx) =>
|
||||
res(
|
||||
ctx.json(
|
||||
|
||||
@@ -32,7 +32,7 @@ const accountsResponse: ListAccounts200 = {
|
||||
},
|
||||
};
|
||||
|
||||
/** Response shape for GET /cloud_integrations/aws/accounts/:accountId/services/:serviceId (used by ServiceDetails). */
|
||||
/** Response shape for GET /cloud_integrations/aws/services/:serviceId (used by ServiceDetails). */
|
||||
const buildServiceDetailsResponse = (
|
||||
serviceId: string,
|
||||
initialConfigLogsS3Buckets: Record<string, string[]> = {},
|
||||
|
||||
@@ -225,7 +225,7 @@ function BodyTitleRenderer({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Settings style={{ marginRight: 8 }} className="hover-reveal" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuContent>
|
||||
<div data-log-detail-ignore="true">
|
||||
{menuItems.map((item) => (
|
||||
<DropdownMenuItem
|
||||
|
||||
@@ -18,8 +18,6 @@ interface TooltipHeaderProps {
|
||||
showTooltipHeader: boolean;
|
||||
isPinned: boolean;
|
||||
activeItem: TooltipContentItem | null;
|
||||
headerRowClassName?: string;
|
||||
dateFormat?: string;
|
||||
}
|
||||
|
||||
export default function TooltipHeader({
|
||||
@@ -28,8 +26,6 @@ export default function TooltipHeader({
|
||||
showTooltipHeader,
|
||||
isPinned,
|
||||
activeItem,
|
||||
headerRowClassName,
|
||||
dateFormat = DATE_TIME_FORMATS.MONTH_DATETIME_SECONDS,
|
||||
}: TooltipHeaderProps): JSX.Element {
|
||||
const { timezone: userTimezone } = useTimezone();
|
||||
const resolvedTimezone = timezone?.value ?? userTimezone.value;
|
||||
@@ -48,13 +44,12 @@ export default function TooltipHeader({
|
||||
}
|
||||
return dayjs(timestamp * 1000)
|
||||
.tz(resolvedTimezone)
|
||||
.format(dateFormat);
|
||||
.format(DATE_TIME_FORMATS.MONTH_DATETIME_SECONDS);
|
||||
}, [
|
||||
resolvedTimezone,
|
||||
uPlotInstance.data,
|
||||
uPlotInstance.cursor.idx,
|
||||
showTooltipHeader,
|
||||
dateFormat,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -63,7 +58,7 @@ export default function TooltipHeader({
|
||||
data-testid="uplot-tooltip-header-container"
|
||||
>
|
||||
{showTooltipHeader && headerTitle && (
|
||||
<div className={cx(Styles.headerRow, headerRowClassName)}>
|
||||
<div className={Styles.headerRow}>
|
||||
<span>{headerTitle}</span>
|
||||
{isPinned && (
|
||||
<div className={cx(Styles.status)} data-testid="uplot-tooltip-status">
|
||||
|
||||
@@ -1,36 +1,29 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FullScreenHandle } from 'react-full-screen';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import {
|
||||
ClipboardCopy,
|
||||
Configure,
|
||||
Ellipsis,
|
||||
FileJson,
|
||||
Fullscreen,
|
||||
LockKeyhole,
|
||||
PenLine,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from '@signozhq/icons';
|
||||
import { Popover } from 'antd';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||
import type { MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DeleteButton } from 'container/ListOfDashboard/TableComponents/DeleteButton';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard';
|
||||
import history from 'lib/history';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import ConfirmDeleteDialog from '../../components/ConfirmDeleteDialog/ConfirmDeleteDialog';
|
||||
import DashboardSettings from '../../DashboardSettings';
|
||||
import SettingsDrawer from '../SettingsDrawer';
|
||||
import styles from '../DashboardDescription.module.scss';
|
||||
|
||||
interface DashboardActionsProps {
|
||||
interface Props {
|
||||
dashboard: DashboardtypesGettableDashboardV2DTO;
|
||||
handle: FullScreenHandle;
|
||||
isDashboardLocked: boolean;
|
||||
@@ -52,19 +45,17 @@ function DashboardActions({
|
||||
onAddPanel,
|
||||
onLockToggle,
|
||||
onOpenRename,
|
||||
}: DashboardActionsProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const { t } = useTranslation(['dashboard', 'common']);
|
||||
|
||||
const id = dashboard.id ?? '';
|
||||
const id = dashboard.id;
|
||||
const title = dashboard.spec?.display?.name ?? '';
|
||||
|
||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] =
|
||||
const [isDashboardSettingsOpen, setIsDashboardSettingsOpen] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const [state, setCopy] = useCopyToClipboard();
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState<boolean>(false);
|
||||
const deleteDashboardMutation = useDeleteDashboard(id);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.error) {
|
||||
@@ -75,12 +66,9 @@ function DashboardActions({
|
||||
}
|
||||
}, [state.error, state.value, t]);
|
||||
|
||||
const dashboardDataJSON = useCallback(
|
||||
(): string => JSON.stringify(dashboard, null, 2),
|
||||
[dashboard],
|
||||
);
|
||||
const dashboardDataJSON = (): string => JSON.stringify(dashboard, null, 2);
|
||||
|
||||
const exportJSON = useCallback((): void => {
|
||||
const exportJSON = (): void => {
|
||||
const blob = new Blob([dashboardDataJSON()], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
@@ -90,141 +78,119 @@ function DashboardActions({
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}, [dashboardDataJSON, title]);
|
||||
|
||||
const handleConfirmDelete = useCallback((): void => {
|
||||
deleteDashboardMutation.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
setIsDeleteOpen(false);
|
||||
history.replace(ROUTES.ALL_DASHBOARD);
|
||||
},
|
||||
});
|
||||
}, [deleteDashboardMutation]);
|
||||
|
||||
const menuItems = useMemo<MenuItem[]>(() => {
|
||||
const editGroup: MenuItem[] = [];
|
||||
if (!isDashboardLocked && editDashboard) {
|
||||
editGroup.push({
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
icon: <PenLine size={14} />,
|
||||
onClick: onOpenRename,
|
||||
});
|
||||
}
|
||||
if (isAuthor || user.role === USER_ROLES.ADMIN) {
|
||||
editGroup.push({
|
||||
key: 'lock',
|
||||
label: isDashboardLocked ? 'Unlock dashboard' : 'Lock dashboard',
|
||||
icon: <LockKeyhole size={14} />,
|
||||
disabled: dashboard.createdBy === 'integration',
|
||||
onClick: onLockToggle,
|
||||
});
|
||||
}
|
||||
editGroup.push({
|
||||
key: 'fullscreen',
|
||||
label: 'Full screen',
|
||||
icon: <Fullscreen size={14} />,
|
||||
onClick: handle.enter,
|
||||
});
|
||||
|
||||
const exportGroup: MenuItem[] = [
|
||||
{
|
||||
key: 'export',
|
||||
label: 'Export JSON',
|
||||
icon: <FileJson size={14} />,
|
||||
onClick: exportJSON,
|
||||
},
|
||||
{
|
||||
key: 'copy',
|
||||
label: 'Copy as JSON',
|
||||
icon: <ClipboardCopy size={14} />,
|
||||
onClick: (): void => setCopy(dashboardDataJSON()),
|
||||
},
|
||||
];
|
||||
|
||||
const dangerGroup: MenuItem[] = [
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete dashboard',
|
||||
icon: <Trash2 size={14} />,
|
||||
danger: true,
|
||||
onClick: (): void => setIsDeleteOpen(true),
|
||||
},
|
||||
];
|
||||
|
||||
return [editGroup, exportGroup, dangerGroup]
|
||||
.filter((group) => group.length > 0)
|
||||
.flatMap((group, index) =>
|
||||
index > 0 ? [{ type: 'divider' } as MenuItem, ...group] : group,
|
||||
);
|
||||
}, [
|
||||
isDashboardLocked,
|
||||
editDashboard,
|
||||
isAuthor,
|
||||
user.role,
|
||||
dashboard.createdBy,
|
||||
onOpenRename,
|
||||
onLockToggle,
|
||||
handle.enter,
|
||||
exportJSON,
|
||||
setCopy,
|
||||
dashboardDataJSON,
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.rightSection}>
|
||||
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
|
||||
<DropdownMenuSimple menu={{ items: menuItems }}>
|
||||
<Popover
|
||||
open={isDashboardSettingsOpen}
|
||||
arrow={false}
|
||||
onOpenChange={(visible): void => setIsDashboardSettingsOpen(visible)}
|
||||
rootClassName={styles.dashboardSettings}
|
||||
content={
|
||||
<div className={styles.menuContent}>
|
||||
<section className={styles.section1}>
|
||||
{(isAuthor || user.role === USER_ROLES.ADMIN) && (
|
||||
<TooltipSimple
|
||||
title={
|
||||
dashboard.createdBy === 'integration'
|
||||
? 'Dashboards created by integrations cannot be unlocked'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
prefix={<LockKeyhole size={14} />}
|
||||
disabled={dashboard.createdBy === 'integration'}
|
||||
onClick={(): void => {
|
||||
setIsDashboardSettingsOpen(false);
|
||||
onLockToggle();
|
||||
}}
|
||||
testId="lock-unlock-dashboard"
|
||||
>
|
||||
{isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
|
||||
{!isDashboardLocked && editDashboard && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
prefix={<PenLine size={14} />}
|
||||
onClick={(): void => {
|
||||
onOpenRename();
|
||||
setIsDashboardSettingsOpen(false);
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
prefix={<Fullscreen size={14} />}
|
||||
onClick={handle.enter}
|
||||
>
|
||||
Full screen
|
||||
</Button>
|
||||
</section>
|
||||
<section className={styles.section2}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
prefix={<FileJson size={14} />}
|
||||
onClick={(): void => {
|
||||
exportJSON();
|
||||
setIsDashboardSettingsOpen(false);
|
||||
}}
|
||||
>
|
||||
Export JSON
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
prefix={<ClipboardCopy size={14} />}
|
||||
onClick={(): void => {
|
||||
setCopy(dashboardDataJSON());
|
||||
setIsDashboardSettingsOpen(false);
|
||||
}}
|
||||
>
|
||||
Copy as JSON
|
||||
</Button>
|
||||
</section>
|
||||
<section className={styles.deleteDashboard}>
|
||||
<DeleteButton
|
||||
createdBy={dashboard.createdBy || ''}
|
||||
name={title}
|
||||
id={id}
|
||||
isLocked={isDashboardLocked}
|
||||
routeToListPage
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
prefix={<Ellipsis size={14} />}
|
||||
className={styles.icons}
|
||||
testId="options"
|
||||
/>
|
||||
</DropdownMenuSimple>
|
||||
{!isDashboardLocked && editDashboard && (
|
||||
<>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
prefix={<Configure size="md" />}
|
||||
testId="show-drawer"
|
||||
onClick={(): void => setIsSettingsDrawerOpen(true)}
|
||||
size="md"
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
<SettingsDrawer
|
||||
drawerTitle="Dashboard Configuration"
|
||||
isOpen={isSettingsDrawerOpen}
|
||||
onClose={(): void => setIsSettingsDrawerOpen(false)}
|
||||
>
|
||||
<DashboardSettings dashboard={dashboard} />
|
||||
</SettingsDrawer>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
{!isDashboardLocked && addPanelPermission && (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
className={styles.addPanelBtn}
|
||||
onClick={onAddPanel}
|
||||
prefix={<Plus size="md" />}
|
||||
testId="add-panel-header"
|
||||
size="md"
|
||||
>
|
||||
New Panel
|
||||
</Button>
|
||||
)}
|
||||
<ConfirmDeleteDialog
|
||||
open={isDeleteOpen}
|
||||
title={`Delete dashboard "${title}"?`}
|
||||
description="This action cannot be undone."
|
||||
isLoading={deleteDashboardMutation.isLoading}
|
||||
onConfirm={handleConfirmDelete}
|
||||
onClose={(): void => setIsDeleteOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 45%;
|
||||
height: 40px;
|
||||
|
||||
.dashboardImg {
|
||||
height: 16px;
|
||||
@@ -43,35 +42,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.clickableTitle {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.titleEdit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.titleInput {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.titleEditActionButton {
|
||||
--button-height: auto;
|
||||
--button-padding: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.titleSaveActionButton {
|
||||
--button-border-color: var(--text-forest-700);
|
||||
--button-outlined-foreground: var(--text-forest-700);
|
||||
}
|
||||
|
||||
.publicDashboardIcon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
@@ -84,7 +54,6 @@
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
height: 40px;
|
||||
|
||||
.icons {
|
||||
display: flex;
|
||||
@@ -108,6 +77,41 @@
|
||||
.icons:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.configureButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 93px;
|
||||
height: 34px;
|
||||
padding: 6px;
|
||||
justify-content: center;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 10px; /* 83.333% */
|
||||
letter-spacing: 0.12px;
|
||||
}
|
||||
|
||||
.addPanelBtn {
|
||||
display: flex;
|
||||
width: 119px;
|
||||
height: 34px;
|
||||
padding: 5.937px 11.875px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--primary-foreground);
|
||||
background: var(--primary-background);
|
||||
font-family: Inter;
|
||||
font-size: 11.875px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 17.812px; /* 150% */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +209,95 @@
|
||||
}
|
||||
}
|
||||
|
||||
.deleteModal :global(.ant-modal-confirm-body) {
|
||||
align-items: center;
|
||||
.renameDashboard {
|
||||
:global(.ant-modal-content) {
|
||||
width: 384px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
padding: 0px;
|
||||
|
||||
:global(.ant-modal-header) {
|
||||
height: 52px;
|
||||
padding: 16px;
|
||||
background: var(--l2-background);
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
margin-bottom: 0px;
|
||||
|
||||
:global(.ant-modal-title) {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
width: 349px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-modal-body) {
|
||||
padding: 16px;
|
||||
|
||||
.dashboardContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.nameText {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
}
|
||||
|
||||
.dashboardNameInput {
|
||||
display: flex;
|
||||
padding: 6px 6px 6px 8px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
align-self: stretch;
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-modal-footer) {
|
||||
padding: 16px;
|
||||
margin-top: 0px;
|
||||
|
||||
.dashboardRename {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
gap: 12px;
|
||||
|
||||
.cancelBtn {
|
||||
display: flex;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--l1-border);
|
||||
}
|
||||
|
||||
.renameBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 169px;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--primary-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import { isEmpty } from 'lodash-es';
|
||||
|
||||
import styles from '../DashboardDescription.module.scss';
|
||||
|
||||
interface DashboardMetaProps {
|
||||
interface Props {
|
||||
tags: string[];
|
||||
description: string;
|
||||
}
|
||||
|
||||
function DashboardMeta({ tags, description }: DashboardMetaProps): JSX.Element {
|
||||
function DashboardMeta({ tags, description }: Props): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{tags.length > 0 && (
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
import { KeyboardEvent } from 'react';
|
||||
import { Check, Globe, LockKeyhole, X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Globe, LockKeyhole } from '@signozhq/icons';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
|
||||
import styles from '../DashboardDescription.module.scss';
|
||||
|
||||
interface DashboardTitleProps {
|
||||
interface Props {
|
||||
title: string;
|
||||
image: string;
|
||||
isPublicDashboard: boolean;
|
||||
isDashboardLocked: boolean;
|
||||
isEditable: boolean;
|
||||
isEditing: boolean;
|
||||
draft: string;
|
||||
onDraftChange: (value: string) => void;
|
||||
onStartEdit: () => void;
|
||||
onCommit: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
function DashboardTitle({
|
||||
@@ -27,76 +16,18 @@ function DashboardTitle({
|
||||
image,
|
||||
isPublicDashboard,
|
||||
isDashboardLocked,
|
||||
isEditable,
|
||||
isEditing,
|
||||
draft,
|
||||
onDraftChange,
|
||||
onStartEdit,
|
||||
onCommit,
|
||||
onCancel,
|
||||
}: DashboardTitleProps): JSX.Element {
|
||||
const canEdit = isEditable && !isDashboardLocked;
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
onCommit();
|
||||
} else if (event.key === 'Escape') {
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<div className={styles.leftSection}>
|
||||
<img src={image} alt="dashboard-img" className={styles.dashboardImg} />
|
||||
{isEditing ? (
|
||||
<div className={styles.titleEdit}>
|
||||
<Input
|
||||
autoFocus
|
||||
value={draft}
|
||||
testId="dashboard-title-input"
|
||||
maxLength={120}
|
||||
className={styles.titleInput}
|
||||
onChange={(e): void => onDraftChange(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
size="icon"
|
||||
className={cx(styles.titleEditActionButton, styles.titleSaveActionButton)}
|
||||
aria-label="Save title"
|
||||
testId="dashboard-title-save"
|
||||
onClick={onCommit}
|
||||
>
|
||||
<Check size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="destructive"
|
||||
size="icon"
|
||||
className={styles.titleEditActionButton}
|
||||
aria-label="Cancel title edit"
|
||||
testId="dashboard-title-cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<TooltipSimple title={title.length > 30 ? title : ''}>
|
||||
<Typography.Text
|
||||
className={cx(styles.dashboardTitle, {
|
||||
[styles.clickableTitle]: canEdit,
|
||||
})}
|
||||
data-testid="dashboard-title"
|
||||
onClick={canEdit ? onStartEdit : undefined}
|
||||
>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
<TooltipSimple title={title.length > 30 ? title : ''}>
|
||||
<Typography.Text
|
||||
className={styles.dashboardTitle}
|
||||
data-testid="dashboard-title"
|
||||
>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
</TooltipSimple>
|
||||
|
||||
{isPublicDashboard && (
|
||||
<TooltipSimple title="This dashboard is publicly accessible">
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface UseEditableTitleArgs {
|
||||
value: string;
|
||||
onSave: (next: string) => void;
|
||||
}
|
||||
|
||||
interface UseEditableTitleResult {
|
||||
isEditing: boolean;
|
||||
draft: string;
|
||||
setDraft: (next: string) => void;
|
||||
startEdit: () => void;
|
||||
cancel: () => void;
|
||||
commit: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drives an inline-editable title. The parent owns the canonical `value`; this
|
||||
* hook tracks the in-flight `draft` and whether we're editing. `commit` saves
|
||||
* only when the trimmed draft is non-empty and actually changed. A `cancelled`
|
||||
* ref guards against a blur firing right after Escape from also committing.
|
||||
*/
|
||||
export function useEditableTitle({
|
||||
value,
|
||||
onSave,
|
||||
}: UseEditableTitleArgs): UseEditableTitleResult {
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [draft, setDraft] = useState<string>(value);
|
||||
const cancelled = useRef<boolean>(false);
|
||||
|
||||
// Keep the draft in sync with the canonical value while not editing (e.g.
|
||||
// after a refetch updates the title).
|
||||
useEffect(() => {
|
||||
if (!isEditing) {
|
||||
setDraft(value);
|
||||
}
|
||||
}, [value, isEditing]);
|
||||
|
||||
const startEdit = (): void => {
|
||||
cancelled.current = false;
|
||||
setDraft(value);
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const cancel = (): void => {
|
||||
cancelled.current = true;
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const commit = (): void => {
|
||||
if (cancelled.current) {
|
||||
cancelled.current = false;
|
||||
return;
|
||||
}
|
||||
const trimmed = draft.trim();
|
||||
if (trimmed && trimmed !== value) {
|
||||
onSave(trimmed);
|
||||
}
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
return { isEditing, draft, setDraft, startEdit, cancel, commit };
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Input, Modal } from 'antd';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Check, X } from '@signozhq/icons';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import styles from '../DashboardDescription.module.scss';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
value: string;
|
||||
isLoading: boolean;
|
||||
onChange: (value: string) => void;
|
||||
onRename: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function RenameDashboardModal({
|
||||
open,
|
||||
value,
|
||||
isLoading,
|
||||
onChange,
|
||||
onRename,
|
||||
onClose,
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title="Rename Dashboard"
|
||||
onOk={onRename}
|
||||
onCancel={onClose}
|
||||
rootClassName={styles.renameDashboard}
|
||||
footer={
|
||||
<div className={styles.dashboardRename}>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
prefix={<Check size={14} />}
|
||||
className={styles.renameBtn}
|
||||
onClick={onRename}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Rename Dashboard
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
prefix={<X size={14} />}
|
||||
className={styles.cancelBtn}
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={styles.dashboardContent}>
|
||||
<Typography.Text className={styles.nameText}>
|
||||
Enter a new name
|
||||
</Typography.Text>
|
||||
<Input
|
||||
data-testid="dashboard-name"
|
||||
className={styles.dashboardNameInput}
|
||||
value={value}
|
||||
onChange={(e): void => onChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default RenameDashboardModal;
|
||||
@@ -1,43 +0,0 @@
|
||||
.settingsContainerRoot {
|
||||
:global(.ant-drawer-wrapper-body) {
|
||||
border-left: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
|
||||
:global(.ant-drawer-header) {
|
||||
height: 48px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
padding: 14px 14px 14px 11px;
|
||||
|
||||
:global(.ant-drawer-header-title) {
|
||||
gap: 16px;
|
||||
|
||||
:global(.ant-drawer-title) {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
:global(.ant-drawer-close) {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-inline-end: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-drawer-body) {
|
||||
padding: 16px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { memo, PropsWithChildren, ReactElement } from 'react';
|
||||
import { Drawer } from 'antd';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
|
||||
import styles from './SettingsDrawer.module.scss';
|
||||
|
||||
type SettingsDrawerProps = PropsWithChildren<{
|
||||
drawerTitle: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}>;
|
||||
|
||||
function SettingsDrawer({
|
||||
children,
|
||||
drawerTitle,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: SettingsDrawerProps): JSX.Element {
|
||||
return (
|
||||
<Drawer
|
||||
title={drawerTitle}
|
||||
placement="right"
|
||||
width="50%"
|
||||
onClose={onClose}
|
||||
open={isOpen}
|
||||
rootClassName={styles.settingsContainerRoot}
|
||||
>
|
||||
{/* Need to type cast because of OverlayScrollbar type definition. We should be good once we remove it. */}
|
||||
<OverlayScrollbar>{children as ReactElement}</OverlayScrollbar>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(SettingsDrawer);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { FullScreenHandle } from 'react-full-screen';
|
||||
import { Card } from 'antd';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
@@ -15,7 +15,6 @@ import type {
|
||||
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
@@ -23,7 +22,7 @@ import DashboardHeader from '../components/DashboardHeader/DashboardHeader';
|
||||
import DashboardActions from './DashboardActions/DashboardActions';
|
||||
import DashboardMeta from './DashboardMeta/DashboardMeta';
|
||||
import DashboardTitle from './DashboardTitle/DashboardTitle';
|
||||
import { useEditableTitle } from './DashboardTitle/useEditableTitle';
|
||||
import RenameDashboardModal from './RenameDashboardModal/RenameDashboardModal';
|
||||
|
||||
import styles from './DashboardDescription.module.scss';
|
||||
|
||||
@@ -53,9 +52,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const [editDashboard] = useComponentPermission(['edit_dashboard'], user.role);
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
|
||||
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||
);
|
||||
|
||||
const isAuthor =
|
||||
!!user?.email && !!dashboard.createdBy && dashboard.createdBy === user.email;
|
||||
@@ -63,7 +59,16 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
// V2 public dashboard wiring lives separately; treat as not-public for chrome.
|
||||
const isPublicDashboard = false;
|
||||
|
||||
const handleLockDashboardToggle = useCallback(async (): Promise<void> => {
|
||||
const [isRenameDashboardOpen, setIsRenameDashboardOpen] =
|
||||
useState<boolean>(false);
|
||||
const [updatedTitle, setUpdatedTitle] = useState<string>(title);
|
||||
const [isRenameLoading, setIsRenameLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setUpdatedTitle(title);
|
||||
}, [title]);
|
||||
|
||||
const handleLockDashboardToggle = async (): Promise<void> => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
@@ -79,43 +84,41 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
}
|
||||
}, [id, isDashboardLocked, refetch, showErrorModal]);
|
||||
};
|
||||
|
||||
const onNameSave = useCallback(
|
||||
async (next: string): Promise<void> => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const patch: DashboardtypesJSONPatchOperationDTO[] = [
|
||||
{
|
||||
op: 'replace' as DashboardtypesJSONPatchOperationDTO['op'],
|
||||
path: '/spec/display/name',
|
||||
value: next,
|
||||
},
|
||||
];
|
||||
await patchDashboardV2({ id }, patch);
|
||||
toast.success('Dashboard renamed successfully');
|
||||
refetch();
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
}
|
||||
},
|
||||
[id, refetch, showErrorModal],
|
||||
);
|
||||
const onNameChangeHandler = async (): Promise<void> => {
|
||||
const trimmed = updatedTitle.trim();
|
||||
if (!id || !trimmed || trimmed === title) {
|
||||
setIsRenameDashboardOpen(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsRenameLoading(true);
|
||||
const patch: DashboardtypesJSONPatchOperationDTO[] = [
|
||||
{
|
||||
op: 'replace' as DashboardtypesJSONPatchOperationDTO['op'],
|
||||
path: '/spec/display/name',
|
||||
value: trimmed,
|
||||
},
|
||||
];
|
||||
await patchDashboardV2({ id }, patch);
|
||||
toast.success('Dashboard renamed successfully');
|
||||
setIsRenameDashboardOpen(false);
|
||||
refetch();
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
setIsRenameDashboardOpen(true);
|
||||
} finally {
|
||||
setIsRenameLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const { isEditing, draft, setDraft, startEdit, cancel, commit } =
|
||||
useEditableTitle({
|
||||
value: title,
|
||||
onSave: onNameSave,
|
||||
});
|
||||
|
||||
const onEmptyWidgetHandler = useCallback((): void => {
|
||||
const onEmptyWidgetHandler = (): void => {
|
||||
void logEvent('Dashboard Detail V2: Add new panel clicked', {
|
||||
dashboardId: id,
|
||||
});
|
||||
setIsPanelTypeSelectionModalOpen(true);
|
||||
}, [id, setIsPanelTypeSelectionModalOpen]);
|
||||
toast.info('V2 panel editor coming next');
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={styles.dashboardDescriptionContainer}>
|
||||
@@ -126,13 +129,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
image={image}
|
||||
isPublicDashboard={isPublicDashboard}
|
||||
isDashboardLocked={isDashboardLocked}
|
||||
isEditable={editDashboard}
|
||||
isEditing={isEditing}
|
||||
draft={draft}
|
||||
onDraftChange={setDraft}
|
||||
onStartEdit={startEdit}
|
||||
onCommit={commit}
|
||||
onCancel={cancel}
|
||||
/>
|
||||
<DashboardActions
|
||||
dashboard={dashboard}
|
||||
@@ -143,10 +139,19 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
addPanelPermission={addPanelPermission}
|
||||
onAddPanel={onEmptyWidgetHandler}
|
||||
onLockToggle={handleLockDashboardToggle}
|
||||
onOpenRename={startEdit}
|
||||
onOpenRename={(): void => setIsRenameDashboardOpen(true)}
|
||||
/>
|
||||
</section>
|
||||
<DashboardMeta tags={tags} description={description} />
|
||||
|
||||
<RenameDashboardModal
|
||||
open={isRenameDashboardOpen}
|
||||
value={updatedTitle}
|
||||
isLoading={isRenameLoading}
|
||||
onChange={setUpdatedTitle}
|
||||
onRename={onNameChangeHandler}
|
||||
onClose={(): void => setIsRenameDashboardOpen(false)}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.placeholder {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.tabLabel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
line-height: 1;
|
||||
padding-top: 4px;
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
// eslint-disable-next-line signoz/no-antd-components -- TODO: migrate Radio to @signozhq/ui/radio-group
|
||||
import { Col, Radio, Tooltip } from 'antd';
|
||||
import { ExternalLink, SolidInfoCircle } from '@signozhq/icons';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { Events } from 'constants/events';
|
||||
import { useDashboardCursorSyncMode } from 'hooks/dashboard/useDashboardCursorSyncMode';
|
||||
import { useSyncTooltipFilterMode } from 'hooks/dashboard/useSyncTooltipFilterMode';
|
||||
import {
|
||||
DashboardCursorSync,
|
||||
SyncTooltipFilterMode,
|
||||
} from 'lib/uPlotV2/plugins/TooltipPlugin/types';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
import cx from 'classnames';
|
||||
|
||||
import styles from '../GeneralSettings.module.scss';
|
||||
|
||||
interface CrossPanelSyncProps {
|
||||
dashboardId: string;
|
||||
}
|
||||
|
||||
function CrossPanelSync({ dashboardId }: CrossPanelSyncProps): JSX.Element {
|
||||
const [cursorSyncMode, setCursorSyncMode] =
|
||||
useDashboardCursorSyncMode(dashboardId);
|
||||
const [syncTooltipFilterMode, setSyncTooltipFilterMode] =
|
||||
useSyncTooltipFilterMode(dashboardId);
|
||||
|
||||
return (
|
||||
<Col className={cx(styles.overviewSettings, styles.crossPanelSyncGroup)}>
|
||||
<div className={styles.crossPanelSyncSectionHeader}>
|
||||
<Typography.Text className={styles.crossPanelSyncSectionTitle}>
|
||||
Cross-Panel Sync
|
||||
</Typography.Text>
|
||||
<Tooltip
|
||||
title={
|
||||
<div className={styles.crossPanelSyncTooltipContent}>
|
||||
<strong className={styles.crossPanelSyncTooltipTitle}>
|
||||
Cross-Panel Sync
|
||||
</strong>
|
||||
<span className={styles.crossPanelSyncTooltipDescription}>
|
||||
Sync crosshair and tooltip across all the dashboard panels
|
||||
</span>
|
||||
<a
|
||||
href="https://signoz.io/docs/dashboards/interactivity/#cross-panel-sync"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.crossPanelSyncTooltipDocLink}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLink size={12} />
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
placement="top"
|
||||
mouseEnterDelay={0.5}
|
||||
>
|
||||
<SolidInfoCircle size="md" className={styles.crossPanelSyncInfoIcon} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className={styles.crossPanelSyncRow}>
|
||||
<div className={styles.crossPanelSyncInfo}>
|
||||
<Typography.Text className={styles.crossPanelSyncTitle}>
|
||||
Sync Mode
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.crossPanelSyncDescription}>
|
||||
Sync crosshair and tooltip across all the dashboard panels
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Radio.Group
|
||||
value={cursorSyncMode}
|
||||
onChange={(e): void => {
|
||||
setCursorSyncMode(e.target.value as DashboardCursorSync);
|
||||
}}
|
||||
>
|
||||
<Radio.Button value={DashboardCursorSync.None}>No Sync</Radio.Button>
|
||||
<Radio.Button value={DashboardCursorSync.Crosshair}>
|
||||
Crosshair
|
||||
</Radio.Button>
|
||||
<Radio.Button value={DashboardCursorSync.Tooltip}>Tooltip</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
{cursorSyncMode === DashboardCursorSync.Tooltip && (
|
||||
<div className={styles.crossPanelSyncRow}>
|
||||
<div className={styles.crossPanelSyncInfo}>
|
||||
<Typography.Text className={styles.crossPanelSyncTitle}>
|
||||
Synced Tooltip Series
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.crossPanelSyncDescription}>
|
||||
Show only series that intersect on group-by, or every series with the
|
||||
matching ones highlighted
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Radio.Group
|
||||
value={syncTooltipFilterMode}
|
||||
onChange={(e): void => {
|
||||
void logEvent(Events.TOOLTIP_SYNC_MODE_CHANGED, {
|
||||
path: getAbsoluteUrl(window.location.pathname),
|
||||
mode: e.target.value,
|
||||
});
|
||||
setSyncTooltipFilterMode(e.target.value as SyncTooltipFilterMode);
|
||||
}}
|
||||
>
|
||||
<Radio.Button value={SyncTooltipFilterMode.All}>All</Radio.Button>
|
||||
<Radio.Button value={SyncTooltipFilterMode.Filtered}>
|
||||
Filtered
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default CrossPanelSync;
|
||||
@@ -1,85 +0,0 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
// eslint-disable-next-line signoz/no-antd-components -- TODO: migrate Select/Input to @signozhq/ui
|
||||
import { Col, Input, Select, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddBadges';
|
||||
|
||||
import { Base64Icons } from '../utils';
|
||||
import styles from '../GeneralSettings.module.scss';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface GeneralFormProps {
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
tags: string[];
|
||||
onTitleChange: (value: string) => void;
|
||||
onDescriptionChange: (value: string) => void;
|
||||
onImageChange: (value: string) => void;
|
||||
onTagsChange: Dispatch<SetStateAction<string[]>>;
|
||||
}
|
||||
|
||||
function GeneralForm({
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
tags,
|
||||
onTitleChange,
|
||||
onDescriptionChange,
|
||||
onImageChange,
|
||||
onTagsChange,
|
||||
}: GeneralFormProps): JSX.Element {
|
||||
return (
|
||||
<Col className={styles.overviewSettings}>
|
||||
<Space direction="vertical" className={styles.formSpace}>
|
||||
<div>
|
||||
<Typography className={styles.dashboardName}>Dashboard Name</Typography>
|
||||
<section className={styles.nameIconInput}>
|
||||
<Select
|
||||
defaultActiveFirstOption
|
||||
data-testid="dashboard-image"
|
||||
suffixIcon={null}
|
||||
rootClassName={styles.dashboardImageInput}
|
||||
value={image}
|
||||
onChange={onImageChange}
|
||||
>
|
||||
{Base64Icons.map((icon) => (
|
||||
<Option value={icon} key={icon}>
|
||||
<img
|
||||
src={icon}
|
||||
alt="dashboard-icon"
|
||||
className={styles.listItemImage}
|
||||
/>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
data-testid="dashboard-name"
|
||||
className={styles.dashboardNameInput}
|
||||
value={title}
|
||||
onChange={(e): void => onTitleChange(e.target.value)}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Typography className={styles.dashboardName}>Description</Typography>
|
||||
<Input.TextArea
|
||||
data-testid="dashboard-desc"
|
||||
rows={6}
|
||||
value={description}
|
||||
className={styles.descriptionTextArea}
|
||||
onChange={(e): void => onDescriptionChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography className={styles.dashboardName}>Tags</Typography>
|
||||
<AddTags tags={tags} setTags={onTagsChange} />
|
||||
</div>
|
||||
</Space>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default GeneralForm;
|
||||
@@ -1,238 +0,0 @@
|
||||
.overviewContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.overviewSettings {
|
||||
padding: 16px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.crossPanelSyncGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.formSpace {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 21px;
|
||||
}
|
||||
|
||||
.crossPanelSyncSectionTitle {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.crossPanelSyncSectionHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.crossPanelSyncInfoIcon {
|
||||
cursor: help;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
.crossPanelSyncTooltipContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.crossPanelSyncTooltipTitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.crossPanelSyncTooltipDescription {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.crossPanelSyncTooltipDocLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--primary-background);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.crossPanelSyncRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
& + & {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
.crossPanelSyncInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.crossPanelSyncTitle {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.crossPanelSyncDescription {
|
||||
color: var(--l3-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.nameIconInput {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.dashboardImageInput {
|
||||
:global(.ant-select-selector) {
|
||||
display: flex;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 6px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
border: 1px solid var(--l1-border) !important;
|
||||
background: var(--l3-background) !important;
|
||||
|
||||
:global(.ant-select-selection-item) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&:global(.ant-select-dropdown) {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
:global(.ant-select-item) {
|
||||
padding: 0px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
:global(.ant-select-item-option-content) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listItemImage {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.dashboardNameInput {
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
.dashboardName {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.descriptionTextArea {
|
||||
padding: 6px 6px 6px 8px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
.overviewSettingsFooter {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: -webkit-fill-available;
|
||||
padding: 12px 16px 12px 0px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
height: 32px;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
.unsaved {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.unsavedDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50px;
|
||||
background: var(--primary-background);
|
||||
box-shadow: 0px 0px 6px 0px
|
||||
color-mix(in srgb, var(--primary-background) 40%, transparent);
|
||||
}
|
||||
|
||||
.unsavedChanges {
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.footerActionBtns {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.discardBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.saveBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0px !important;
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Check, X } from '@signozhq/icons';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import styles from '../GeneralSettings.module.scss';
|
||||
|
||||
interface UnsavedChangesFooterProps {
|
||||
count: number;
|
||||
isSaving: boolean;
|
||||
onDiscard: () => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
function UnsavedChangesFooter({
|
||||
count,
|
||||
isSaving,
|
||||
onDiscard,
|
||||
onSave,
|
||||
}: UnsavedChangesFooterProps): JSX.Element {
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
return (
|
||||
<div className={styles.overviewSettingsFooter}>
|
||||
<div className={styles.unsaved}>
|
||||
<div className={styles.unsavedDot} />
|
||||
<Typography.Text className={styles.unsavedChanges}>
|
||||
{count} unsaved change
|
||||
{count > 1 && 's'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className={styles.footerActionBtns}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={isSaving}
|
||||
prefix={<X size={14} />}
|
||||
onClick={onDiscard}
|
||||
className={styles.discardBtn}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
disabled={isSaving}
|
||||
loading={isSaving}
|
||||
prefix={<Check size={14} />}
|
||||
testId="save-dashboard-config"
|
||||
onClick={onSave}
|
||||
className={styles.saveBtn}
|
||||
>
|
||||
{t('save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UnsavedChangesFooter;
|
||||
@@ -1,170 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { patchDashboardV2 } from 'api/generated/services/dashboard';
|
||||
import type {
|
||||
DashboardtypesGettableDashboardV2DTO,
|
||||
DashboardtypesJSONPatchOperationDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { useDashboardStore } from '../../store/useDashboardStore';
|
||||
import CrossPanelSync from './CrossPanelSync/CrossPanelSync';
|
||||
import GeneralForm from './GeneralForm/GeneralForm';
|
||||
import UnsavedChangesFooter from './UnsavedChangesFooter/UnsavedChangesFooter';
|
||||
import { Base64Icons, stringsToTags, tagsToStrings } from './utils';
|
||||
import styles from './GeneralSettings.module.scss';
|
||||
|
||||
interface GeneralSettingsProps {
|
||||
dashboard: DashboardtypesGettableDashboardV2DTO;
|
||||
}
|
||||
|
||||
function GeneralSettings({ dashboard }: GeneralSettingsProps): JSX.Element {
|
||||
const id = dashboard.id;
|
||||
|
||||
const refetch = useDashboardStore((s) => s.refetch);
|
||||
|
||||
const title = dashboard.spec?.display?.name ?? '';
|
||||
const description = dashboard.spec?.display?.description ?? '';
|
||||
const image = dashboard.image || Base64Icons[0];
|
||||
const tagsAsStrings = useMemo(
|
||||
() => tagsToStrings(dashboard.tags ?? []),
|
||||
[dashboard.tags],
|
||||
);
|
||||
|
||||
const [updatedTitle, setUpdatedTitle] = useState<string>(title);
|
||||
const [updatedTags, setUpdatedTags] = useState<string[]>(tagsAsStrings);
|
||||
const [updatedDescription, setUpdatedDescription] =
|
||||
useState<string>(description);
|
||||
const [updatedImage, setUpdatedImage] = useState<string>(image);
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [numberOfUnsavedChanges, setNumberOfUnsavedChanges] =
|
||||
useState<number>(0);
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
// Sync state when dashboard refetches after a save
|
||||
useEffect(() => {
|
||||
setUpdatedTitle(title);
|
||||
setUpdatedDescription(description);
|
||||
setUpdatedImage(image);
|
||||
setUpdatedTags(tagsAsStrings);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dashboard.updatedAt]);
|
||||
|
||||
const buildPatch = useCallback((): DashboardtypesJSONPatchOperationDTO[] => {
|
||||
const ops: DashboardtypesJSONPatchOperationDTO[] = [];
|
||||
const replace = (
|
||||
path: string,
|
||||
value: unknown,
|
||||
): DashboardtypesJSONPatchOperationDTO => ({
|
||||
op: 'replace' as DashboardtypesJSONPatchOperationDTO['op'],
|
||||
path,
|
||||
value,
|
||||
});
|
||||
|
||||
if (updatedTitle !== title) {
|
||||
ops.push(replace('/spec/display/name', updatedTitle));
|
||||
}
|
||||
if (updatedDescription !== description) {
|
||||
ops.push(replace('/spec/display/description', updatedDescription));
|
||||
}
|
||||
if (updatedImage !== image) {
|
||||
ops.push(replace('/image', updatedImage));
|
||||
}
|
||||
if (!isEqual(updatedTags, tagsAsStrings)) {
|
||||
ops.push(replace('/tags', stringsToTags(updatedTags)));
|
||||
}
|
||||
return ops;
|
||||
}, [
|
||||
updatedTitle,
|
||||
title,
|
||||
updatedDescription,
|
||||
description,
|
||||
updatedImage,
|
||||
image,
|
||||
updatedTags,
|
||||
tagsAsStrings,
|
||||
]);
|
||||
|
||||
const onSaveHandler = useCallback(async (): Promise<void> => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const ops = buildPatch();
|
||||
if (ops.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
await patchDashboardV2({ id }, ops);
|
||||
toast.success('Dashboard updated');
|
||||
refetch();
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [id, buildPatch, refetch, showErrorModal]);
|
||||
|
||||
useEffect(() => {
|
||||
let n = 0;
|
||||
const initialValues = [title, description, tagsAsStrings, image];
|
||||
const updatedValues = [
|
||||
updatedTitle,
|
||||
updatedDescription,
|
||||
updatedTags,
|
||||
updatedImage,
|
||||
];
|
||||
initialValues.forEach((val, index) => {
|
||||
if (!isEqual(val, updatedValues[index])) {
|
||||
n += 1;
|
||||
}
|
||||
});
|
||||
setNumberOfUnsavedChanges(n);
|
||||
}, [
|
||||
description,
|
||||
image,
|
||||
tagsAsStrings,
|
||||
title,
|
||||
updatedDescription,
|
||||
updatedImage,
|
||||
updatedTags,
|
||||
updatedTitle,
|
||||
]);
|
||||
|
||||
const discardHandler = useCallback((): void => {
|
||||
setUpdatedTitle(title);
|
||||
setUpdatedImage(image);
|
||||
setUpdatedTags(tagsAsStrings);
|
||||
setUpdatedDescription(description);
|
||||
}, [title, image, tagsAsStrings, description]);
|
||||
|
||||
return (
|
||||
<div className={styles.overviewContent}>
|
||||
<GeneralForm
|
||||
title={updatedTitle}
|
||||
description={updatedDescription}
|
||||
image={updatedImage}
|
||||
tags={updatedTags}
|
||||
onTitleChange={setUpdatedTitle}
|
||||
onDescriptionChange={setUpdatedDescription}
|
||||
onImageChange={setUpdatedImage}
|
||||
onTagsChange={setUpdatedTags}
|
||||
/>
|
||||
<CrossPanelSync dashboardId={id} />
|
||||
{numberOfUnsavedChanges > 0 && (
|
||||
<UnsavedChangesFooter
|
||||
count={numberOfUnsavedChanges}
|
||||
isSaving={isSaving}
|
||||
onDiscard={discardHandler}
|
||||
onSave={onSaveHandler}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GeneralSettings;
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { TagtypesPostableTagDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
|
||||
|
||||
// tag UX, a string with no ':' is round-tripped as `{key: x, value: x}` and
|
||||
// collapsed back to just `x` for display.
|
||||
export function tagsToStrings(tags: TagtypesPostableTagDTO[]): string[] {
|
||||
return tags.map((t) => (t.key === t.value ? t.key : `${t.key}:${t.value}`));
|
||||
}
|
||||
|
||||
export function stringsToTags(tagStrings: string[]): TagtypesPostableTagDTO[] {
|
||||
return tagStrings
|
||||
.map((s) => {
|
||||
const trimmed = s.trim();
|
||||
const idx = trimmed.indexOf(':');
|
||||
if (idx === -1) {
|
||||
return { key: trimmed, value: trimmed };
|
||||
}
|
||||
const key = trimmed.slice(0, idx).trim();
|
||||
const value = trimmed.slice(idx + 1).trim();
|
||||
return { key, value: value || key };
|
||||
})
|
||||
.filter((t) => t.key.length > 0);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Braces, Globe, Table } from '@signozhq/icons';
|
||||
import { Tabs } from '@signozhq/ui/tabs';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import GeneralSettings from './General';
|
||||
import { SettingsTabPlaceholder } from './utils';
|
||||
|
||||
import styles from './DashboardSettings.module.scss';
|
||||
|
||||
interface DashboardSettingsProps {
|
||||
dashboard: DashboardtypesGettableDashboardV2DTO;
|
||||
}
|
||||
|
||||
function tabLabel(icon: JSX.Element, text: string): JSX.Element {
|
||||
return (
|
||||
<span className={styles.tabLabel}>
|
||||
{icon}
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardSettings({ dashboard }: DashboardSettingsProps): JSX.Element {
|
||||
const items = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'general',
|
||||
label: tabLabel(<Table size={14} />, 'General'),
|
||||
children: <GeneralSettings dashboard={dashboard} />,
|
||||
},
|
||||
{
|
||||
key: 'variables',
|
||||
label: tabLabel(<Braces size={14} />, 'Variables'),
|
||||
children: (
|
||||
<SettingsTabPlaceholder message="V2 dashboard variables coming next." />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'public-dashboard',
|
||||
label: tabLabel(<Globe size={14} />, 'Publish'),
|
||||
children: (
|
||||
<SettingsTabPlaceholder message="V2 public dashboard publishing coming next." />
|
||||
),
|
||||
},
|
||||
],
|
||||
[dashboard],
|
||||
);
|
||||
|
||||
return <Tabs defaultValue="general" items={items} />;
|
||||
}
|
||||
|
||||
export default DashboardSettings;
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Empty } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import styles from './DashboardSettings.module.scss';
|
||||
|
||||
/**
|
||||
* TEMPORARY: stand-in for the not-yet-built Variables / Publish settings tabs.
|
||||
* Will be cleaned up later once those tabs ship their real content.
|
||||
*/
|
||||
export function SettingsTabPlaceholder({
|
||||
message,
|
||||
}: {
|
||||
message: string;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className={styles.placeholder}>
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={<Typography.Text>{message}</Typography.Text>}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
.emptyState {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 48px 16px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.emoji {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.welcome {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.08px;
|
||||
}
|
||||
|
||||
.welcomeInfo {
|
||||
color: var(--l3-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.addPanel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border: 1px dashed var(--l1-border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.addPanelText {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
|
||||
.icon {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.addPanelCopy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.addPanelTitle {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.addPanelInfo {
|
||||
color: var(--l3-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||
|
||||
import dashboardEmojiUrl from '@/assets/Icons/dashboard_emoji.svg';
|
||||
import landscapeUrl from '@/assets/Icons/landscape.svg';
|
||||
|
||||
import styles from './DashboardEmptyState.module.scss';
|
||||
|
||||
interface DashboardEmptyStateProps {
|
||||
canAddPanel: boolean;
|
||||
}
|
||||
|
||||
function DashboardEmptyState({
|
||||
canAddPanel,
|
||||
}: DashboardEmptyStateProps): JSX.Element {
|
||||
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
|
||||
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||
);
|
||||
|
||||
return (
|
||||
<section className={styles.emptyState}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.heading}>
|
||||
<img src={dashboardEmojiUrl} alt="" className={styles.emoji} />
|
||||
<Typography.Text className={styles.welcome}>
|
||||
Welcome to your new dashboard
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.welcomeInfo}>
|
||||
Follow the steps to populate it with data and share with your teammates
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className={styles.addPanel}>
|
||||
<div className={styles.addPanelText}>
|
||||
<img src={landscapeUrl} alt="" className={styles.icon} />
|
||||
<div className={styles.addPanelCopy}>
|
||||
<Typography.Text className={styles.addPanelTitle}>
|
||||
Add panels
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.addPanelInfo}>
|
||||
Add panels to visualize your data
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
{canAddPanel && (
|
||||
<Button
|
||||
color="primary"
|
||||
prefix={<Plus size="md" />}
|
||||
onClick={(): void => setIsPanelTypeSelectionModalOpen(true)}
|
||||
testId="add-panel"
|
||||
>
|
||||
New Panel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardEmptyState;
|
||||
@@ -4,7 +4,7 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: var(--bg-ink-400, #0b0c0e);
|
||||
border: 1px solid var(--l1-border);
|
||||
border: 1px solid var(--bg-slate-400, #1d212d);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
border-bottom: 1px solid var(--bg-slate-400, #1d212d);
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--bg-vanilla-400, #8993ae);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -12,15 +12,7 @@ import type { MovePanelArgs } from './hooks/useMovePanelToSection';
|
||||
import PanelActionsMenu from './PanelActionsMenu/PanelActionsMenu';
|
||||
import styles from './Panel.module.scss';
|
||||
|
||||
/** Panel action context — present together only in editable sectioned mode. */
|
||||
export interface PanelActionsConfig {
|
||||
currentLayoutIndex: number;
|
||||
sections: DashboardSection[];
|
||||
onMovePanel: (args: MovePanelArgs) => void;
|
||||
onDeletePanel: (args: DeletePanelArgs) => void;
|
||||
}
|
||||
|
||||
interface PanelProps {
|
||||
interface Props {
|
||||
panel: DashboardtypesPanelDTO | undefined;
|
||||
panelId: string;
|
||||
/**
|
||||
@@ -29,16 +21,22 @@ interface PanelProps {
|
||||
* data. Currently unused on purpose.
|
||||
*/
|
||||
isVisible?: boolean;
|
||||
/** Move/delete actions — present only in editable sectioned mode. */
|
||||
panelActions?: PanelActionsConfig;
|
||||
/** Section actions — present only in editable sectioned mode. */
|
||||
currentLayoutIndex?: number;
|
||||
sections?: DashboardSection[];
|
||||
onMovePanel?: (args: MovePanelArgs) => void;
|
||||
onDeletePanel?: (args: DeletePanelArgs) => void;
|
||||
}
|
||||
|
||||
function Panel({
|
||||
panel,
|
||||
panelId,
|
||||
isVisible,
|
||||
panelActions,
|
||||
}: PanelProps): JSX.Element {
|
||||
currentLayoutIndex,
|
||||
sections,
|
||||
onMovePanel,
|
||||
onDeletePanel,
|
||||
}: Props): JSX.Element {
|
||||
const name = panel?.spec?.display?.name || `Panel ${panelId.slice(0, 6)}`;
|
||||
const description = panel?.spec?.display?.description;
|
||||
const kind = panel?.spec?.plugin?.kind?.replace(/^signoz\//, '') ?? 'unknown';
|
||||
@@ -67,13 +65,13 @@ function Panel({
|
||||
</Typography.Text>
|
||||
<Badge className={styles.badge}>{kind}</Badge>
|
||||
</div>
|
||||
{panelActions ? (
|
||||
{currentLayoutIndex !== undefined && (onMovePanel || onDeletePanel) ? (
|
||||
<PanelActionsMenu
|
||||
panelId={panelId}
|
||||
currentLayoutIndex={panelActions.currentLayoutIndex}
|
||||
sections={panelActions.sections}
|
||||
onMovePanel={panelActions.onMovePanel}
|
||||
onDeletePanel={panelActions.onDeletePanel}
|
||||
currentLayoutIndex={currentLayoutIndex}
|
||||
sections={sections ?? []}
|
||||
onMovePanel={onMovePanel}
|
||||
onDeletePanel={onDeletePanel}
|
||||
/>
|
||||
) : (
|
||||
<EllipsisVertical size={14} />
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--bg-vanilla-400, #8993ae);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l2-background);
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { EllipsisVertical, FolderInput, Trash2 } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||
import type { MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
|
||||
@@ -9,7 +8,7 @@ import type { DeletePanelArgs } from '../hooks/useDeletePanel';
|
||||
import type { MovePanelArgs } from '../hooks/useMovePanelToSection';
|
||||
import styles from './PanelActionsMenu.module.scss';
|
||||
|
||||
interface PanelActionsMenuProps {
|
||||
interface Props {
|
||||
panelId: string;
|
||||
currentLayoutIndex: number;
|
||||
sections: DashboardSection[];
|
||||
@@ -23,7 +22,7 @@ function PanelActionsMenu({
|
||||
sections,
|
||||
onMovePanel,
|
||||
onDeletePanel,
|
||||
}: PanelActionsMenuProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const items = useMemo<MenuItem[]>(() => {
|
||||
const result: MenuItem[] = [];
|
||||
|
||||
@@ -76,11 +75,8 @@ function PanelActionsMenu({
|
||||
|
||||
return (
|
||||
<DropdownMenuSimple menu={{ items }}>
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={styles.trigger}
|
||||
aria-label="Panel actions"
|
||||
data-testid={`panel-actions-${panelId}`}
|
||||
@@ -91,7 +87,7 @@ function PanelActionsMenu({
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
>
|
||||
<EllipsisVertical size={14} />
|
||||
</Button>
|
||||
</button>
|
||||
</DropdownMenuSimple>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: var(--bg-ink-400, #0b0c0e);
|
||||
border: 1px solid var(--l1-border);
|
||||
border: 1px solid var(--bg-slate-400, #1d212d);
|
||||
border-radius: 4px;
|
||||
color: var(--l1-foreground);
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
||||
|
||||
@@ -1,10 +1,48 @@
|
||||
import { Modal } from 'antd';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import {
|
||||
BarChart,
|
||||
ChartLine,
|
||||
ChartPie,
|
||||
Hash,
|
||||
List,
|
||||
Table,
|
||||
} from '@signozhq/icons';
|
||||
|
||||
import { PANEL_TYPES } from './constants';
|
||||
import styles from './PanelTypeSelectionModal.module.scss';
|
||||
|
||||
interface PanelTypeSelectionModalProps {
|
||||
interface PanelType {
|
||||
pluginKind: string;
|
||||
label: string;
|
||||
icon: JSX.Element;
|
||||
}
|
||||
|
||||
const PANEL_TYPES: PanelType[] = [
|
||||
{
|
||||
pluginKind: 'signoz/TimeSeriesPanel',
|
||||
label: 'Time Series',
|
||||
icon: <ChartLine size={16} />,
|
||||
},
|
||||
{ pluginKind: 'signoz/NumberPanel', label: 'Value', icon: <Hash size={16} /> },
|
||||
{ pluginKind: 'signoz/TablePanel', label: 'Table', icon: <Table size={16} /> },
|
||||
{
|
||||
pluginKind: 'signoz/BarChartPanel',
|
||||
label: 'Bar Chart',
|
||||
icon: <BarChart size={16} />,
|
||||
},
|
||||
{
|
||||
pluginKind: 'signoz/PieChartPanel',
|
||||
label: 'Pie Chart',
|
||||
icon: <ChartPie size={16} />,
|
||||
},
|
||||
{
|
||||
pluginKind: 'signoz/HistogramPanel',
|
||||
label: 'Histogram',
|
||||
icon: <BarChart size={16} />,
|
||||
},
|
||||
{ pluginKind: 'signoz/ListPanel', label: 'List', icon: <List size={16} /> },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSelect: (pluginKind: string) => void;
|
||||
@@ -14,7 +52,7 @@ function PanelTypeSelectionModal({
|
||||
open,
|
||||
onClose,
|
||||
onSelect,
|
||||
}: PanelTypeSelectionModalProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
@@ -25,17 +63,16 @@ function PanelTypeSelectionModal({
|
||||
>
|
||||
<div className={styles.grid}>
|
||||
{PANEL_TYPES.map((type) => (
|
||||
<Button
|
||||
<button
|
||||
key={type.pluginKind}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className={styles.typeButton}
|
||||
data-testid={`panel-type-${type.pluginKind}`}
|
||||
onClick={(): void => onSelect(type.pluginKind)}
|
||||
>
|
||||
{type.icon}
|
||||
{type.label}
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import {
|
||||
BarChart,
|
||||
ChartLine,
|
||||
ChartPie,
|
||||
Hash,
|
||||
List,
|
||||
Table,
|
||||
} from '@signozhq/icons';
|
||||
|
||||
import type { PanelType } from './types';
|
||||
|
||||
export const PANEL_TYPES: PanelType[] = [
|
||||
{
|
||||
pluginKind: 'signoz/TimeSeriesPanel',
|
||||
label: 'Time Series',
|
||||
icon: <ChartLine size={16} />,
|
||||
},
|
||||
{ pluginKind: 'signoz/NumberPanel', label: 'Value', icon: <Hash size={16} /> },
|
||||
{ pluginKind: 'signoz/TablePanel', label: 'Table', icon: <Table size={16} /> },
|
||||
{
|
||||
pluginKind: 'signoz/BarChartPanel',
|
||||
label: 'Bar Chart',
|
||||
icon: <BarChart size={16} />,
|
||||
},
|
||||
{
|
||||
pluginKind: 'signoz/PieChartPanel',
|
||||
label: 'Pie Chart',
|
||||
icon: <ChartPie size={16} />,
|
||||
},
|
||||
{
|
||||
pluginKind: 'signoz/HistogramPanel',
|
||||
label: 'Histogram',
|
||||
icon: <BarChart size={16} />,
|
||||
},
|
||||
{ pluginKind: 'signoz/ListPanel', label: 'List', icon: <List size={16} /> },
|
||||
];
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface PanelType {
|
||||
pluginKind: string;
|
||||
label: string;
|
||||
icon: JSX.Element;
|
||||
}
|
||||
@@ -36,6 +36,9 @@ export function useAddPanelToSection({
|
||||
|
||||
return useCallback(
|
||||
async ({ layoutIndex, pluginKind }: AddPanelArgs): Promise<void> => {
|
||||
if (!dashboardId) {
|
||||
return;
|
||||
}
|
||||
const target = sections.find((s) => s.layoutIndex === layoutIndex);
|
||||
if (!target) {
|
||||
return;
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--l1-border);
|
||||
border: 1px dashed var(--bg-slate-400, #1d212d);
|
||||
border-radius: 4px;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--bg-vanilla-400, #8993ae);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-robin-500);
|
||||
color: var(--l1-foreground);
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import type { DashboardtypesLayoutDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import type { DashboardSection } from '../../../utils';
|
||||
@@ -11,7 +10,7 @@ import styles from './AddSectionControl.module.scss';
|
||||
|
||||
const DEFAULT_SECTION_TITLE = 'New section';
|
||||
|
||||
interface AddSectionControlProps {
|
||||
interface Props {
|
||||
sections: DashboardSection[];
|
||||
layouts: DashboardtypesLayoutDTO[] | undefined | null;
|
||||
isSectioned: boolean;
|
||||
@@ -21,7 +20,7 @@ function AddSectionControl({
|
||||
sections,
|
||||
layouts,
|
||||
isSectioned,
|
||||
}: AddSectionControlProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const [isMigrationOpen, setIsMigrationOpen] = useState(false);
|
||||
const { addSection } = useAddSection({ layouts });
|
||||
const { migrate, isSaving } = useFirstSectionMigration({ sections });
|
||||
@@ -31,31 +30,30 @@ function AddSectionControl({
|
||||
const needsMigration =
|
||||
!isSectioned && sections.some((s) => s.items.length > 0);
|
||||
|
||||
const handleClick = useCallback((): void => {
|
||||
const handleClick = (): void => {
|
||||
if (needsMigration) {
|
||||
setIsMigrationOpen(true);
|
||||
return;
|
||||
}
|
||||
void addSection(DEFAULT_SECTION_TITLE);
|
||||
}, [needsMigration, addSection]);
|
||||
};
|
||||
|
||||
const handleConfirmMigration = useCallback(async (): Promise<void> => {
|
||||
const handleConfirmMigration = async (): Promise<void> => {
|
||||
await migrate(DEFAULT_SECTION_TITLE);
|
||||
setIsMigrationOpen(false);
|
||||
}, [migrate]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className={styles.addButton}
|
||||
onClick={handleClick}
|
||||
data-testid="add-section"
|
||||
>
|
||||
<Plus size={14} />
|
||||
Add section
|
||||
</Button>
|
||||
</button>
|
||||
<FirstSectionMigrationModal
|
||||
open={isMigrationOpen}
|
||||
isSaving={isSaving}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Modal } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
interface FirstSectionMigrationModalProps {
|
||||
interface Props {
|
||||
open: boolean;
|
||||
isSaving: boolean;
|
||||
onClose: () => void;
|
||||
@@ -18,7 +18,7 @@ function FirstSectionMigrationModal({
|
||||
isSaving,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: FirstSectionMigrationModalProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { Modal } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
|
||||
interface RenameSectionModalProps {
|
||||
interface Props {
|
||||
open: boolean;
|
||||
initialValue: string;
|
||||
isSaving: boolean;
|
||||
@@ -16,7 +16,7 @@ function RenameSectionModal({
|
||||
isSaving,
|
||||
onClose,
|
||||
onSubmit,
|
||||
}: RenameSectionModalProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const [value, setValue] = useState<string>(initialValue);
|
||||
|
||||
// Reseed the field each time the modal opens.
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
.section {
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dragging {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.deleteModal :global(.ant-modal-confirm-body) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.emptySection {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 24px 12px;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Modal } from 'antd';
|
||||
|
||||
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
||||
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||
|
||||
import ConfirmDeleteDialog from '../../../components/ConfirmDeleteDialog/ConfirmDeleteDialog';
|
||||
import type { DashboardSection } from '../../../utils';
|
||||
import type { AddPanelArgs } from '../../Panel/hooks/useAddPanelToSection';
|
||||
import type { DeletePanelArgs } from '../../Panel/hooks/useDeletePanel';
|
||||
@@ -22,7 +19,7 @@ import SectionHeader, {
|
||||
} from '../SectionHeader/SectionHeader';
|
||||
import styles from './Section.module.scss';
|
||||
|
||||
interface SectionProps {
|
||||
interface Props {
|
||||
section: DashboardSection;
|
||||
/** Adds a panel to this section; present only in editable sectioned mode. */
|
||||
onAddPanel?: (args: AddPanelArgs) => void;
|
||||
@@ -41,12 +38,8 @@ function Section({
|
||||
onMovePanel,
|
||||
onDeletePanel,
|
||||
dragHandle,
|
||||
}: SectionProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const isEditable = useDashboardStore((s) => s.isEditable);
|
||||
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
|
||||
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||
);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
// Placeholder signal for lazy panel query-loading (consumed in a later PR):
|
||||
// true once the section scrolls into (or near) the viewport.
|
||||
@@ -61,30 +54,30 @@ function Section({
|
||||
layoutIndex: section.layoutIndex,
|
||||
});
|
||||
|
||||
const handleRenameSubmit = useCallback(
|
||||
async (title: string): Promise<void> => {
|
||||
const ok = await rename(title);
|
||||
if (ok) {
|
||||
setIsRenaming(false);
|
||||
}
|
||||
},
|
||||
[rename],
|
||||
);
|
||||
const handleRenameSubmit = async (title: string): Promise<void> => {
|
||||
const ok = await rename(title);
|
||||
if (ok) {
|
||||
setIsRenaming(false);
|
||||
}
|
||||
};
|
||||
|
||||
const [isAddingPanel, setIsAddingPanel] = useState(false);
|
||||
const handleSelectPanelType = useCallback(
|
||||
(pluginKind: string): void => {
|
||||
onAddPanel?.({ layoutIndex: section.layoutIndex, pluginKind });
|
||||
setIsAddingPanel(false);
|
||||
},
|
||||
[onAddPanel, section.layoutIndex],
|
||||
);
|
||||
const handleSelectPanelType = (pluginKind: string): void => {
|
||||
onAddPanel?.({ layoutIndex: section.layoutIndex, pluginKind });
|
||||
setIsAddingPanel(false);
|
||||
};
|
||||
|
||||
const { deleteSection } = useDeleteSection({ section });
|
||||
const handleDeleteSection = useCallback((): void => {
|
||||
void deleteSection();
|
||||
setIsDeleteOpen(false);
|
||||
}, [deleteSection]);
|
||||
const confirmDeleteSection = (): void => {
|
||||
Modal.confirm({
|
||||
title: `Delete section "${section.title ?? ''}"?`,
|
||||
content: 'Panels in this section will be removed.',
|
||||
okText: 'Delete',
|
||||
okButtonProps: { danger: true },
|
||||
centered: true,
|
||||
onOk: () => deleteSection(),
|
||||
});
|
||||
};
|
||||
|
||||
const grid = (
|
||||
<SectionGrid
|
||||
@@ -125,35 +118,13 @@ function Section({
|
||||
onToggle={toggle}
|
||||
repeatVariable={section.repeatVariable}
|
||||
dragHandle={dragHandle}
|
||||
actions={
|
||||
isEditable
|
||||
? {
|
||||
onRename: (): void => setIsRenaming(true),
|
||||
onAddPanel: (): void => setIsAddingPanel(true),
|
||||
onDeleteSection: (): void => setIsDeleteOpen(true),
|
||||
}
|
||||
: undefined
|
||||
onRename={isEditable ? (): void => setIsRenaming(true) : undefined}
|
||||
onAddPanel={
|
||||
isEditable && onAddPanel ? (): void => setIsAddingPanel(true) : undefined
|
||||
}
|
||||
onDeleteSection={isEditable ? confirmDeleteSection : undefined}
|
||||
/>
|
||||
{open &&
|
||||
(section.items.length > 0 ? (
|
||||
grid
|
||||
) : (
|
||||
<div className={styles.emptySection}>
|
||||
{isEditable && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="dashed"
|
||||
color="secondary"
|
||||
prefix={<Plus size="md" />}
|
||||
onClick={(): void => setIsPanelTypeSelectionModalOpen(true)}
|
||||
testId={`section-add-panel-${section.id}`}
|
||||
>
|
||||
New Panel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{open ? grid : null}
|
||||
<RenameSectionModal
|
||||
open={isRenaming}
|
||||
initialValue={section.title}
|
||||
@@ -166,13 +137,6 @@ function Section({
|
||||
onClose={(): void => setIsAddingPanel(false)}
|
||||
onSelect={handleSelectPanelType}
|
||||
/>
|
||||
<ConfirmDeleteDialog
|
||||
open={isDeleteOpen}
|
||||
title={`Delete section "${section.title ?? ''}"?`}
|
||||
description="Panels in this section will be removed."
|
||||
onConfirm={handleDeleteSection}
|
||||
onClose={(): void => setIsDeleteOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--bg-vanilla-400, #8993ae);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l2-background);
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useMemo } from 'react';
|
||||
import { EllipsisVertical, PenLine, Plus, Trash2 } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||
import type { MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
|
||||
import styles from './SectionActionsMenu.module.scss';
|
||||
|
||||
interface SectionActionsMenuProps {
|
||||
interface Props {
|
||||
sectionId: string;
|
||||
onAddPanel?: () => void;
|
||||
onRename?: () => void;
|
||||
@@ -18,7 +17,7 @@ function SectionActionsMenu({
|
||||
onAddPanel,
|
||||
onRename,
|
||||
onDeleteSection,
|
||||
}: SectionActionsMenuProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const items = useMemo<MenuItem[]>(() => {
|
||||
const result: MenuItem[] = [];
|
||||
if (onAddPanel) {
|
||||
@@ -54,17 +53,14 @@ function SectionActionsMenu({
|
||||
|
||||
return (
|
||||
<DropdownMenuSimple menu={{ items }}>
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={styles.trigger}
|
||||
aria-label="Section actions"
|
||||
data-testid={`dashboard-section-actions-${sectionId}`}
|
||||
>
|
||||
<EllipsisVertical size={14} />
|
||||
</Button>
|
||||
</button>
|
||||
</DropdownMenuSimple>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { DashboardSection } from '../../../utils';
|
||||
import SectionHeader from '../SectionHeader/SectionHeader';
|
||||
import styles from './SectionDragPreview.module.scss';
|
||||
|
||||
interface SectionDragPreviewProps {
|
||||
interface Props {
|
||||
section: DashboardSection;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ interface SectionDragPreviewProps {
|
||||
* dragged. Deliberately header-only (no react-grid-layout) so the overlay is
|
||||
* cheap and never triggers RGL width re-measurement.
|
||||
*/
|
||||
function SectionDragPreview({ section }: SectionDragPreviewProps): JSX.Element {
|
||||
function SectionDragPreview({ section }: Props): JSX.Element {
|
||||
const panelCount = section.items.length;
|
||||
const title = `${section.title ?? ''} · ${panelCount} ${
|
||||
panelCount === 1 ? 'panel' : 'panels'
|
||||
|
||||
@@ -11,7 +11,7 @@ import styles from './SectionGrid.module.scss';
|
||||
|
||||
const ResponsiveGridLayout = WidthProvider(GridLayout);
|
||||
|
||||
interface SectionGridProps {
|
||||
interface Props {
|
||||
items: DashboardSection['items'];
|
||||
layoutIndex: number;
|
||||
/** Forwarded to panels — true when the parent section is in the viewport. */
|
||||
@@ -29,7 +29,7 @@ function SectionGrid({
|
||||
sections,
|
||||
onMovePanel,
|
||||
onDeletePanel,
|
||||
}: SectionGridProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const isEditable = useDashboardStore((s) => s.isEditable);
|
||||
const rglLayout = useMemo<Layout[]>(
|
||||
() =>
|
||||
@@ -66,16 +66,10 @@ function SectionGrid({
|
||||
panel={item.panel}
|
||||
panelId={item.id}
|
||||
isVisible={isVisible}
|
||||
panelActions={
|
||||
isEditable && onMovePanel && onDeletePanel
|
||||
? {
|
||||
currentLayoutIndex: layoutIndex,
|
||||
sections: sections ?? [],
|
||||
onMovePanel,
|
||||
onDeletePanel,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
currentLayoutIndex={layoutIndex}
|
||||
sections={isEditable ? sections : undefined}
|
||||
onMovePanel={isEditable ? onMovePanel : undefined}
|
||||
onDeletePanel={isEditable ? onDeletePanel : undefined}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
padding: 8px 12px;
|
||||
|
||||
&.headerOpen {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--bg-vanilla-400, #8993ae);
|
||||
cursor: grab;
|
||||
|
||||
&:active {
|
||||
@@ -33,8 +33,7 @@
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
// Muted chevron; the title below carries the prominent heading color.
|
||||
color: var(--l2-foreground);
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
min-width: 0;
|
||||
@@ -42,8 +41,6 @@
|
||||
|
||||
.title {
|
||||
margin-left: 4px;
|
||||
color: var(--l1-foreground);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { DraggableAttributes } from '@dnd-kit/core';
|
||||
import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
|
||||
import { ChevronDown, ChevronRight, GripVertical } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
|
||||
@@ -14,14 +13,7 @@ export interface SectionDragHandle {
|
||||
setActivatorNodeRef: (element: HTMLElement | null) => void;
|
||||
}
|
||||
|
||||
/** Editable-mode section actions — present together or not at all. */
|
||||
export interface SectionHeaderActions {
|
||||
onRename: () => void;
|
||||
onAddPanel: () => void;
|
||||
onDeleteSection: () => void;
|
||||
}
|
||||
|
||||
interface SectionHeaderProps {
|
||||
interface Props {
|
||||
sectionId: string;
|
||||
title: string;
|
||||
open: boolean;
|
||||
@@ -29,8 +21,9 @@ interface SectionHeaderProps {
|
||||
repeatVariable?: string;
|
||||
/** Provided by SortableSection in sectioned mode; absent for untitled/free-flow. */
|
||||
dragHandle?: SectionDragHandle;
|
||||
/** Present only in editable mode; absent (read-only) when locked/no-permission. */
|
||||
actions?: SectionHeaderActions;
|
||||
onRename?: () => void;
|
||||
onAddPanel?: () => void;
|
||||
onDeleteSection?: () => void;
|
||||
}
|
||||
|
||||
function SectionHeader({
|
||||
@@ -40,16 +33,16 @@ function SectionHeader({
|
||||
onToggle,
|
||||
repeatVariable,
|
||||
dragHandle,
|
||||
actions,
|
||||
}: SectionHeaderProps): JSX.Element {
|
||||
onRename,
|
||||
onAddPanel,
|
||||
onDeleteSection,
|
||||
}: Props): JSX.Element {
|
||||
const hasActions = !!(onAddPanel || onRename || onDeleteSection);
|
||||
return (
|
||||
<div className={cx(styles.header, { [styles.headerOpen]: open })}>
|
||||
{dragHandle ? (
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={styles.dragHandle}
|
||||
ref={dragHandle.setActivatorNodeRef}
|
||||
aria-label="Drag to reorder section"
|
||||
@@ -58,12 +51,10 @@ function SectionHeader({
|
||||
{...dragHandle.listeners}
|
||||
>
|
||||
<GripVertical size={14} />
|
||||
</Button>
|
||||
</button>
|
||||
) : null}
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className={styles.toggle}
|
||||
onClick={onToggle}
|
||||
data-testid={`dashboard-section-toggle-${sectionId}`}
|
||||
@@ -75,13 +66,13 @@ function SectionHeader({
|
||||
(repeats per ${repeatVariable})
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</Button>
|
||||
{actions ? (
|
||||
</button>
|
||||
{hasActions ? (
|
||||
<SectionActionsMenu
|
||||
sectionId={sectionId}
|
||||
onAddPanel={actions.onAddPanel}
|
||||
onRename={actions.onRename}
|
||||
onDeleteSection={actions.onDeleteSection}
|
||||
onAddPanel={onAddPanel}
|
||||
onRename={onRename}
|
||||
onDeleteSection={onDeleteSection}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -20,12 +20,12 @@ import Section from './Section/Section';
|
||||
import SectionDragPreview from './SectionDragPreview/SectionDragPreview';
|
||||
import SortableSection from './SortableSection';
|
||||
|
||||
interface SectionListProps {
|
||||
interface Props {
|
||||
sections: DashboardSection[];
|
||||
layouts: DashboardtypesLayoutDTO[] | undefined | null;
|
||||
}
|
||||
|
||||
function SectionList({ sections, layouts }: SectionListProps): JSX.Element {
|
||||
function SectionList({ sections, layouts }: Props): JSX.Element {
|
||||
const isEditable = useDashboardStore((s) => s.isEditable);
|
||||
|
||||
const {
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { DeletePanelArgs } from '../Panel/hooks/useDeletePanel';
|
||||
import type { MovePanelArgs } from '../Panel/hooks/useMovePanelToSection';
|
||||
import Section from './Section/Section';
|
||||
|
||||
interface SortableSectionProps {
|
||||
interface Props {
|
||||
section: DashboardSection;
|
||||
sections: DashboardSection[];
|
||||
onAddPanel: (args: AddPanelArgs) => void;
|
||||
@@ -21,7 +21,7 @@ function SortableSection({
|
||||
onAddPanel,
|
||||
onMovePanel,
|
||||
onDeletePanel,
|
||||
}: SortableSectionProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
|
||||
import { Empty } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type {
|
||||
DashboardtypesLayoutDTO,
|
||||
DashboardtypesPanelDTO,
|
||||
@@ -7,7 +9,7 @@ import type {
|
||||
|
||||
import { useDashboardStore } from '../store/useDashboardStore';
|
||||
import { layoutsToSections } from '../utils';
|
||||
import DashboardEmptyState from './DashboardEmptyState/DashboardEmptyState';
|
||||
import AddSectionControl from './Section/AddSectionControl/AddSectionControl';
|
||||
import Section from './Section/Section/Section';
|
||||
import SectionList from './Section/SectionList';
|
||||
import styles from './PanelsAndSectionsLayout.module.scss';
|
||||
@@ -15,15 +17,12 @@ import styles from './PanelsAndSectionsLayout.module.scss';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
|
||||
interface PanelsAndSectionsLayoutProps {
|
||||
interface Props {
|
||||
layouts: DashboardtypesLayoutDTO[];
|
||||
panels: Record<string, DashboardtypesPanelDTO | undefined>;
|
||||
}
|
||||
|
||||
function PanelsAndSectionsLayout({
|
||||
layouts,
|
||||
panels,
|
||||
}: PanelsAndSectionsLayoutProps): JSX.Element {
|
||||
function PanelsAndSectionsLayout({ layouts, panels }: Props): JSX.Element {
|
||||
const isEditable = useDashboardStore((s) => s.isEditable);
|
||||
|
||||
const sections = useMemo(
|
||||
@@ -41,7 +40,16 @@ function PanelsAndSectionsLayout({
|
||||
|
||||
const renderContent = (): ReactNode => {
|
||||
if (isEmpty) {
|
||||
return <DashboardEmptyState canAddPanel={isEditable} />;
|
||||
return (
|
||||
<div className={styles.emptyState}>
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={
|
||||
<Typography.Text>No panels in this dashboard yet</Typography.Text>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSectioned) {
|
||||
@@ -53,7 +61,18 @@ function PanelsAndSectionsLayout({
|
||||
));
|
||||
};
|
||||
|
||||
return <div className={styles.body}>{renderContent()}</div>;
|
||||
return (
|
||||
<div className={styles.body}>
|
||||
{renderContent()}
|
||||
{isEditable ? (
|
||||
<AddSectionControl
|
||||
sections={sections}
|
||||
layouts={layouts}
|
||||
isSectioned={isSectioned}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PanelsAndSectionsLayout;
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
.body {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Trash2, X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
|
||||
import styles from './ConfirmDeleteDialog.module.scss';
|
||||
|
||||
interface ConfirmDeleteDialogProps {
|
||||
open: boolean;
|
||||
title: string;
|
||||
description: ReactNode;
|
||||
confirmLabel?: string;
|
||||
isLoading?: boolean;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared destructive-confirm dialog built on @signozhq/ui DialogWrapper (not
|
||||
* antd Modal), so it inherits the design-system styling/theme. Used by the
|
||||
* dashboard and section delete flows.
|
||||
*/
|
||||
function ConfirmDeleteDialog({
|
||||
open,
|
||||
title,
|
||||
description,
|
||||
confirmLabel = 'Delete',
|
||||
isLoading = false,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: ConfirmDeleteDialogProps): JSX.Element {
|
||||
const footer = (
|
||||
<div className={styles.footer}>
|
||||
<Button variant="solid" color="secondary" onClick={onClose}>
|
||||
<X size={12} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
loading={isLoading}
|
||||
onClick={onConfirm}
|
||||
testId="confirm-delete"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<DialogWrapper
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
title={title}
|
||||
width="narrow"
|
||||
showCloseButton={false}
|
||||
footer={footer}
|
||||
>
|
||||
<div className={styles.body}>{description}</div>
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfirmDeleteDialog;
|
||||
@@ -5,23 +5,26 @@
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
max-width: 80%;
|
||||
padding-left: 8px;
|
||||
|
||||
.linkToPreviousPage {
|
||||
// Collapse the design-system Button's fixed-height/padding box so it hugs
|
||||
// the label like inline text (the breadcrumb is text, not a chunky button).
|
||||
--button-height: auto;
|
||||
--button-padding: 0;
|
||||
--button-gap: 4px;
|
||||
.dashboardBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
padding: 0px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.currentPage {
|
||||
.dashboardBtn:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.idBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
@@ -43,9 +46,12 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.currentPage:hover {
|
||||
:global(.ant-btn-icon) {
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
}
|
||||
.idBtn:hover {
|
||||
background: color-mix(in srgb, var(--bg-robin-400) 10%, transparent);
|
||||
color: var(--bg-robin-300);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import { useCallback } from 'react';
|
||||
import { LayoutGrid } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import getSessionStorageApi from 'api/browser/sessionstorage/get';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY } from 'hooks/dashboard/useDashboardsListQueryParams';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { LayoutGrid } from '@signozhq/icons';
|
||||
|
||||
import styles from './DashboardBreadcrumbs.module.scss';
|
||||
|
||||
interface DashboardBreadcrumbsProps {
|
||||
interface Props {
|
||||
title: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
function DashboardBreadcrumbs({
|
||||
title,
|
||||
image,
|
||||
}: DashboardBreadcrumbsProps): JSX.Element {
|
||||
function DashboardBreadcrumbs({ title, image }: Props): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const goToListPage = useCallback(() => {
|
||||
@@ -39,23 +35,20 @@ function DashboardBreadcrumbs({
|
||||
<div className={styles.dashboardBreadcrumbs}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
prefix={<LayoutGrid size={14} />}
|
||||
className={styles.dashboardBtn}
|
||||
onClick={goToListPage}
|
||||
className={styles.linkToPreviousPage}
|
||||
testId="dashboard-breadcrumb-list"
|
||||
>
|
||||
Dashboard
|
||||
Dashboard /
|
||||
</Button>
|
||||
<div>/</div>
|
||||
<div className={styles.currentPage}>
|
||||
<Button variant="ghost" className={styles.idBtn}>
|
||||
<img
|
||||
src={image}
|
||||
alt="dashboard-icon"
|
||||
className={styles.dashboardIconImage}
|
||||
/>
|
||||
<Typography.Text>{title}</Typography.Text>
|
||||
</div>
|
||||
{title}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import DashboardBreadcrumbs from './DashboardBreadcrumbs';
|
||||
|
||||
import styles from './DashboardHeader.module.scss';
|
||||
|
||||
interface DashboardHeaderProps {
|
||||
interface Props {
|
||||
title: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
function DashboardHeader({ title, image }: DashboardHeaderProps): JSX.Element {
|
||||
function DashboardHeader({ title, image }: Props): JSX.Element {
|
||||
return (
|
||||
<div className={styles.dashboardHeader}>
|
||||
<DashboardBreadcrumbs title={title} image={image} />
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useEffect, useMemo } from 'react';
|
||||
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
|
||||
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import PanelTypeSelectionModal from 'container/DashboardContainer/PanelTypeSelectionModal';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
@@ -11,15 +10,12 @@ import PanelsAndSectionsLayout from './PanelsAndSectionsLayout';
|
||||
import { useDashboardStore } from './store/useDashboardStore';
|
||||
import styles from './DashboardContainer.module.scss';
|
||||
|
||||
interface DashboardContainerProps {
|
||||
interface Props {
|
||||
dashboard: DashboardtypesGettableDashboardV2DTO;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
function DashboardContainer({
|
||||
dashboard,
|
||||
refetch,
|
||||
}: DashboardContainerProps): JSX.Element {
|
||||
function DashboardContainer({ dashboard, refetch }: Props): JSX.Element {
|
||||
const fullScreenHandle = useFullScreenHandle();
|
||||
|
||||
const { user } = useAppContext();
|
||||
@@ -47,9 +43,6 @@ function DashboardContainer({
|
||||
/>
|
||||
<PanelsAndSectionsLayout layouts={layouts} panels={panels} />
|
||||
</div>
|
||||
{/* Shared panel-type picker (V1 component): opened from any "New Panel"
|
||||
trigger; navigates to the widget editor route on selection. */}
|
||||
<PanelTypeSelectionModal />
|
||||
</FullScreen>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
View Billing
|
||||
</Button>
|
||||
|
||||
<RefreshPaymentStatus />
|
||||
<RefreshPaymentStatus btnShape="round" />
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ function WorkspaceSuspended(): JSX.Element {
|
||||
>
|
||||
{t('continueMyJourney')}
|
||||
</Button>
|
||||
<RefreshPaymentStatus />
|
||||
<RefreshPaymentStatus btnShape="round" />
|
||||
</Flex>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
@@ -826,10 +826,4 @@ body.ai-assistant-panel-open {
|
||||
:root {
|
||||
--input-focus-outline-width: 0;
|
||||
--radius-2: 4px;
|
||||
|
||||
// @signozhq/ui dropdown content portals to body with a default z-index of 50,
|
||||
// antd defines it at z-index of 1050. Keeping this till we have components from both
|
||||
// design libraries.
|
||||
--dropdown-menu-content-z-index: 1050;
|
||||
--dropdown-menu-sub-content-z-index: 1050;
|
||||
}
|
||||
|
||||
7
go.mod
7
go.mod
@@ -43,7 +43,7 @@ require (
|
||||
github.com/openfga/api/proto v0.0.0-20260319214821-f153694bfc20
|
||||
github.com/openfga/language/pkg/go v0.2.1
|
||||
github.com/opentracing/opentracing-go v1.2.0
|
||||
github.com/perses/spec v0.1.2
|
||||
github.com/perses/perses v0.53.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/alertmanager v0.31.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
@@ -138,8 +138,9 @@ require (
|
||||
github.com/huandu/go-clone v1.7.3 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/muhlemmer/gu v0.3.1 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/nxadm/tail v1.4.11 // indirect
|
||||
github.com/perses/common v0.30.2 // indirect
|
||||
github.com/prometheus/client_golang/exp v0.0.0-20260325093428-d8591d0db856 // indirect
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
|
||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.15.1 // indirect
|
||||
@@ -150,6 +151,8 @@ require (
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/zitadel/oidc/v3 v3.45.4 // indirect
|
||||
github.com/zitadel/schema v1.3.2 // indirect
|
||||
go.opentelemetry.io/collector/client v1.54.0 // indirect
|
||||
go.opentelemetry.io/collector/config/configoptional v1.50.0 // indirect
|
||||
go.opentelemetry.io/collector/config/configretry v1.50.0 // indirect
|
||||
|
||||
16
go.sum
16
go.sum
@@ -332,7 +332,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
@@ -833,6 +832,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
|
||||
github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
@@ -842,6 +843,8 @@ github.com/natefinch/wrap v0.2.0 h1:IXzc/pw5KqxJv55gV0lSOcKHYuEZPGbQrOOXr/bamRk=
|
||||
github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm4/KAS8=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nexucis/lamenv v0.5.2 h1:tK/u3XGhCq9qIoVNcXsK9LZb8fKopm0A5weqSRvHd7M=
|
||||
github.com/nexucis/lamenv v0.5.2/go.mod h1:HusJm6ltmmT7FMG8A750mOLuME6SHCsr2iFYxp5fFi0=
|
||||
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
|
||||
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
|
||||
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
|
||||
@@ -904,8 +907,10 @@ github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko
|
||||
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/perses/spec v0.1.2 h1:yGoygcR3ZusuGDCmRMwsVXCMvMwi1qZndKV6NYNreEw=
|
||||
github.com/perses/spec v0.1.2/go.mod h1:NoGI5jmGwRdkdPgyYSZJTBL4/Py+dqIPKS2QV8NOvGE=
|
||||
github.com/perses/common v0.30.2 h1:RAiVxUpX76lTCb4X7pfcXSvYdXQmZwKi4oDKAEO//u0=
|
||||
github.com/perses/common v0.30.2/go.mod h1:DFtur1QPah2/ChXbKKhw7djYdwNgz27s5fPKpiK0Xao=
|
||||
github.com/perses/perses v0.53.1 h1:9VY/6p9QWrZwPSV7qiwTMSOsgcB37Lb1AXKT0ORXc6I=
|
||||
github.com/perses/perses v0.53.1/go.mod h1:ro8fsgBkHYOdrL/MV+fdP9mflKzYCy/+gcbxiaReI/A=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
|
||||
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
@@ -1166,6 +1171,10 @@ github.com/zeebo/assert v1.3.1 h1:vukIABvugfNMZMQO1ABsyQDJDTVQbn+LWSMy1ol1h6A=
|
||||
github.com/zeebo/assert v1.3.1/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
github.com/zitadel/oidc/v3 v3.45.4 h1:GKyWaPRVQ8sCu9XgJ3NgNGtG52FzwVJpzXjIUG2+YrI=
|
||||
github.com/zitadel/oidc/v3 v3.45.4/go.mod h1:XALmFXS9/kSom9B6uWin1yJ2WTI/E4Ti5aXJdewAVEs=
|
||||
github.com/zitadel/schema v1.3.2 h1:gfJvt7dOMfTmxzhscZ9KkapKo3Nei3B6cAxjav+lyjI=
|
||||
github.com/zitadel/schema v1.3.2/go.mod h1:IZmdfF9Wu62Zu6tJJTH3UsArevs3Y4smfJIj3L8fzxw=
|
||||
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU=
|
||||
@@ -1610,7 +1619,6 @@ golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
|
||||
@@ -26,7 +26,7 @@ type Alertmanager interface {
|
||||
PutAlerts(context.Context, string, alertmanagertypes.PostableAlerts) error
|
||||
|
||||
// TestReceiver sends a test alert to a receiver.
|
||||
TestReceiver(context.Context, string, *alertmanagertypes.Receiver) error
|
||||
TestReceiver(context.Context, string, alertmanagertypes.Receiver) error
|
||||
|
||||
// TestAlert sends an alert to a list of receivers.
|
||||
TestAlert(ctx context.Context, orgID string, ruleID string, receiversMap map[*alertmanagertypes.PostableAlert][]string) error
|
||||
@@ -41,10 +41,10 @@ type Alertmanager interface {
|
||||
GetChannelByID(context.Context, string, valuer.UUID) (*alertmanagertypes.Channel, error)
|
||||
|
||||
// UpdateChannel updates a channel for the organization.
|
||||
UpdateChannelByReceiverAndID(context.Context, string, *alertmanagertypes.Receiver, valuer.UUID) error
|
||||
UpdateChannelByReceiverAndID(context.Context, string, alertmanagertypes.Receiver, valuer.UUID) error
|
||||
|
||||
// CreateChannel creates a channel for the organization.
|
||||
CreateChannel(context.Context, string, *alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)
|
||||
CreateChannel(context.Context, string, alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)
|
||||
|
||||
// DeleteChannelByID deletes a channel for the organization.
|
||||
DeleteChannelByID(context.Context, string, valuer.UUID) error
|
||||
|
||||
@@ -26,8 +26,8 @@ var customNotifierIntegrations = []string{
|
||||
msteamsv2.Integration,
|
||||
}
|
||||
|
||||
func NewReceiverIntegrations(nc *alertmanagertypes.Receiver, tmpl *template.Template, logger *slog.Logger, templater alertmanagertypes.Templater) ([]notify.Integration, error) {
|
||||
upstreamIntegrations, err := receiver.BuildReceiverIntegrations(*nc.Receiver, tmpl, logger)
|
||||
func NewReceiverIntegrations(nc alertmanagertypes.Receiver, tmpl *template.Template, logger *slog.Logger, templater alertmanagertypes.Templater) ([]notify.Integration, error) {
|
||||
upstreamIntegrations, err := receiver.BuildReceiverIntegrations(nc, tmpl, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -275,11 +275,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
|
||||
server.logger.InfoContext(ctx, "skipping creation of receiver not referenced by any route", slog.String("receiver", rcv.Name))
|
||||
continue
|
||||
}
|
||||
extendedRcv, err := alertmanagerConfig.GetReceiver(rcv.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
integrations, err := alertmanagernotify.NewReceiverIntegrations(extendedRcv, server.tmpl, server.logger, server.templater)
|
||||
integrations, err := alertmanagernotify.NewReceiverIntegrations(rcv, server.tmpl, server.logger, server.templater)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -354,7 +350,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *Server) TestReceiver(ctx context.Context, receiver *alertmanagertypes.Receiver) error {
|
||||
func (server *Server) TestReceiver(ctx context.Context, receiver alertmanagertypes.Receiver) error {
|
||||
testAlert := alertmanagertypes.NewTestAlert(receiver, time.Now(), time.Now())
|
||||
return alertmanagertypes.TestReceiver(ctx, receiver, alertmanagernotify.NewReceiverIntegrations, server.alertmanagerConfig, server.tmpl, server.logger, server.templater, testAlert.Labels, testAlert)
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func TestServerTestReceiverTypeWebhook(t *testing.T) {
|
||||
webhookURL, err := url.Parse("http://" + webhookListener.Addr().String() + "/webhook")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = server.TestReceiver(context.Background(), &alertmanagertypes.Receiver{Receiver: &config.Receiver{
|
||||
err = server.TestReceiver(context.Background(), alertmanagertypes.Receiver{
|
||||
Name: "test-receiver",
|
||||
WebhookConfigs: []*config.WebhookConfig{
|
||||
{
|
||||
@@ -83,7 +83,7 @@ func TestServerTestReceiverTypeWebhook(t *testing.T) {
|
||||
URL: config.SecretTemplateURL(webhookURL.String()),
|
||||
},
|
||||
},
|
||||
}})
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, requestBody.String(), "test-receiver")
|
||||
@@ -101,7 +101,7 @@ func TestServerPutAlerts(t *testing.T) {
|
||||
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, amConfig.CreateReceiver(&alertmanagertypes.Receiver{Receiver: &config.Receiver{
|
||||
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
|
||||
Name: "test-receiver",
|
||||
WebhookConfigs: []*config.WebhookConfig{
|
||||
{
|
||||
@@ -109,7 +109,7 @@ func TestServerPutAlerts(t *testing.T) {
|
||||
URL: config.SecretTemplateURL("http://localhost/test-receiver"),
|
||||
},
|
||||
},
|
||||
}}))
|
||||
}))
|
||||
|
||||
require.NoError(t, server.SetConfig(context.Background(), amConfig))
|
||||
|
||||
@@ -181,7 +181,7 @@ func TestServerTestAlert(t *testing.T) {
|
||||
webhook2URL, err := url.Parse("http://" + webhook2Listener.Addr().String() + "/webhook")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, amConfig.CreateReceiver(&alertmanagertypes.Receiver{Receiver: &config.Receiver{
|
||||
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
|
||||
Name: "receiver-1",
|
||||
WebhookConfigs: []*config.WebhookConfig{
|
||||
{
|
||||
@@ -189,9 +189,9 @@ func TestServerTestAlert(t *testing.T) {
|
||||
URL: config.SecretTemplateURL(webhook1URL.String()),
|
||||
},
|
||||
},
|
||||
}}))
|
||||
}))
|
||||
|
||||
require.NoError(t, amConfig.CreateReceiver(&alertmanagertypes.Receiver{Receiver: &config.Receiver{
|
||||
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
|
||||
Name: "receiver-2",
|
||||
WebhookConfigs: []*config.WebhookConfig{
|
||||
{
|
||||
@@ -199,7 +199,7 @@ func TestServerTestAlert(t *testing.T) {
|
||||
URL: config.SecretTemplateURL(webhook2URL.String()),
|
||||
},
|
||||
},
|
||||
}}))
|
||||
}))
|
||||
|
||||
require.NoError(t, server.SetConfig(context.Background(), amConfig))
|
||||
defer func() {
|
||||
@@ -273,7 +273,7 @@ func TestServerTestAlertContinuesOnFailure(t *testing.T) {
|
||||
webhookURL, err := url.Parse("http://" + webhookListener.Addr().String() + "/webhook")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, amConfig.CreateReceiver(&alertmanagertypes.Receiver{Receiver: &config.Receiver{
|
||||
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
|
||||
Name: "working-receiver",
|
||||
WebhookConfigs: []*config.WebhookConfig{
|
||||
{
|
||||
@@ -281,9 +281,9 @@ func TestServerTestAlertContinuesOnFailure(t *testing.T) {
|
||||
URL: config.SecretTemplateURL(webhookURL.String()),
|
||||
},
|
||||
},
|
||||
}}))
|
||||
}))
|
||||
|
||||
require.NoError(t, amConfig.CreateReceiver(&alertmanagertypes.Receiver{Receiver: &config.Receiver{
|
||||
require.NoError(t, amConfig.CreateReceiver(alertmanagertypes.Receiver{
|
||||
Name: "failing-receiver",
|
||||
WebhookConfigs: []*config.WebhookConfig{
|
||||
{
|
||||
@@ -291,7 +291,7 @@ func TestServerTestAlertContinuesOnFailure(t *testing.T) {
|
||||
URL: config.SecretTemplateURL("http://localhost:1/webhook"),
|
||||
},
|
||||
},
|
||||
}}))
|
||||
}))
|
||||
|
||||
require.NoError(t, server.SetConfig(context.Background(), amConfig))
|
||||
defer func() {
|
||||
|
||||
@@ -155,7 +155,7 @@ func (_c *MockAlertmanager_Config_Call) RunAndReturn(run func() alertmanagerserv
|
||||
}
|
||||
|
||||
// CreateChannel provides a mock function for the type MockAlertmanager
|
||||
func (_mock *MockAlertmanager) CreateChannel(context1 context.Context, s string, v *alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error) {
|
||||
func (_mock *MockAlertmanager) CreateChannel(context1 context.Context, s string, v alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error) {
|
||||
ret := _mock.Called(context1, s, v)
|
||||
|
||||
if len(ret) == 0 {
|
||||
@@ -164,17 +164,17 @@ func (_mock *MockAlertmanager) CreateChannel(context1 context.Context, s string,
|
||||
|
||||
var r0 *alertmanagertypes.Channel
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, *alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)); ok {
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)); ok {
|
||||
return returnFunc(context1, s, v)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, *alertmanagertypes.Receiver) *alertmanagertypes.Channel); ok {
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alertmanagertypes.Receiver) *alertmanagertypes.Channel); ok {
|
||||
r0 = returnFunc(context1, s, v)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*alertmanagertypes.Channel)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, *alertmanagertypes.Receiver) error); ok {
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, alertmanagertypes.Receiver) error); ok {
|
||||
r1 = returnFunc(context1, s, v)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
@@ -190,12 +190,12 @@ type MockAlertmanager_CreateChannel_Call struct {
|
||||
// CreateChannel is a helper method to define mock.On call
|
||||
// - context1 context.Context
|
||||
// - s string
|
||||
// - v *alertmanagertypes.Receiver
|
||||
// - v alertmanagertypes.Receiver
|
||||
func (_e *MockAlertmanager_Expecter) CreateChannel(context1 interface{}, s interface{}, v interface{}) *MockAlertmanager_CreateChannel_Call {
|
||||
return &MockAlertmanager_CreateChannel_Call{Call: _e.mock.On("CreateChannel", context1, s, v)}
|
||||
}
|
||||
|
||||
func (_c *MockAlertmanager_CreateChannel_Call) Run(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver)) *MockAlertmanager_CreateChannel_Call {
|
||||
func (_c *MockAlertmanager_CreateChannel_Call) Run(run func(context1 context.Context, s string, v alertmanagertypes.Receiver)) *MockAlertmanager_CreateChannel_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
@@ -205,9 +205,9 @@ func (_c *MockAlertmanager_CreateChannel_Call) Run(run func(context1 context.Con
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 *alertmanagertypes.Receiver
|
||||
var arg2 alertmanagertypes.Receiver
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(*alertmanagertypes.Receiver)
|
||||
arg2 = args[2].(alertmanagertypes.Receiver)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
@@ -223,7 +223,7 @@ func (_c *MockAlertmanager_CreateChannel_Call) Return(channel *alertmanagertypes
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAlertmanager_CreateChannel_Call) RunAndReturn(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)) *MockAlertmanager_CreateChannel_Call {
|
||||
func (_c *MockAlertmanager_CreateChannel_Call) RunAndReturn(run func(context1 context.Context, s string, v alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error)) *MockAlertmanager_CreateChannel_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -1624,7 +1624,7 @@ func (_c *MockAlertmanager_TestAlert_Call) RunAndReturn(run func(ctx context.Con
|
||||
}
|
||||
|
||||
// TestReceiver provides a mock function for the type MockAlertmanager
|
||||
func (_mock *MockAlertmanager) TestReceiver(context1 context.Context, s string, v *alertmanagertypes.Receiver) error {
|
||||
func (_mock *MockAlertmanager) TestReceiver(context1 context.Context, s string, v alertmanagertypes.Receiver) error {
|
||||
ret := _mock.Called(context1, s, v)
|
||||
|
||||
if len(ret) == 0 {
|
||||
@@ -1632,7 +1632,7 @@ func (_mock *MockAlertmanager) TestReceiver(context1 context.Context, s string,
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, *alertmanagertypes.Receiver) error); ok {
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alertmanagertypes.Receiver) error); ok {
|
||||
r0 = returnFunc(context1, s, v)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
@@ -1648,12 +1648,12 @@ type MockAlertmanager_TestReceiver_Call struct {
|
||||
// TestReceiver is a helper method to define mock.On call
|
||||
// - context1 context.Context
|
||||
// - s string
|
||||
// - v *alertmanagertypes.Receiver
|
||||
// - v alertmanagertypes.Receiver
|
||||
func (_e *MockAlertmanager_Expecter) TestReceiver(context1 interface{}, s interface{}, v interface{}) *MockAlertmanager_TestReceiver_Call {
|
||||
return &MockAlertmanager_TestReceiver_Call{Call: _e.mock.On("TestReceiver", context1, s, v)}
|
||||
}
|
||||
|
||||
func (_c *MockAlertmanager_TestReceiver_Call) Run(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver)) *MockAlertmanager_TestReceiver_Call {
|
||||
func (_c *MockAlertmanager_TestReceiver_Call) Run(run func(context1 context.Context, s string, v alertmanagertypes.Receiver)) *MockAlertmanager_TestReceiver_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
@@ -1663,9 +1663,9 @@ func (_c *MockAlertmanager_TestReceiver_Call) Run(run func(context1 context.Cont
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 *alertmanagertypes.Receiver
|
||||
var arg2 alertmanagertypes.Receiver
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(*alertmanagertypes.Receiver)
|
||||
arg2 = args[2].(alertmanagertypes.Receiver)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
@@ -1681,7 +1681,7 @@ func (_c *MockAlertmanager_TestReceiver_Call) Return(err error) *MockAlertmanage
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAlertmanager_TestReceiver_Call) RunAndReturn(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver) error) *MockAlertmanager_TestReceiver_Call {
|
||||
func (_c *MockAlertmanager_TestReceiver_Call) RunAndReturn(run func(context1 context.Context, s string, v alertmanagertypes.Receiver) error) *MockAlertmanager_TestReceiver_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -1750,7 +1750,7 @@ func (_c *MockAlertmanager_UpdateAllRoutePoliciesByRuleId_Call) RunAndReturn(run
|
||||
}
|
||||
|
||||
// UpdateChannelByReceiverAndID provides a mock function for the type MockAlertmanager
|
||||
func (_mock *MockAlertmanager) UpdateChannelByReceiverAndID(context1 context.Context, s string, v *alertmanagertypes.Receiver, uUID valuer.UUID) error {
|
||||
func (_mock *MockAlertmanager) UpdateChannelByReceiverAndID(context1 context.Context, s string, v alertmanagertypes.Receiver, uUID valuer.UUID) error {
|
||||
ret := _mock.Called(context1, s, v, uUID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
@@ -1758,7 +1758,7 @@ func (_mock *MockAlertmanager) UpdateChannelByReceiverAndID(context1 context.Con
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, *alertmanagertypes.Receiver, valuer.UUID) error); ok {
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alertmanagertypes.Receiver, valuer.UUID) error); ok {
|
||||
r0 = returnFunc(context1, s, v, uUID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
@@ -1774,13 +1774,13 @@ type MockAlertmanager_UpdateChannelByReceiverAndID_Call struct {
|
||||
// UpdateChannelByReceiverAndID is a helper method to define mock.On call
|
||||
// - context1 context.Context
|
||||
// - s string
|
||||
// - v *alertmanagertypes.Receiver
|
||||
// - v alertmanagertypes.Receiver
|
||||
// - uUID valuer.UUID
|
||||
func (_e *MockAlertmanager_Expecter) UpdateChannelByReceiverAndID(context1 interface{}, s interface{}, v interface{}, uUID interface{}) *MockAlertmanager_UpdateChannelByReceiverAndID_Call {
|
||||
return &MockAlertmanager_UpdateChannelByReceiverAndID_Call{Call: _e.mock.On("UpdateChannelByReceiverAndID", context1, s, v, uUID)}
|
||||
}
|
||||
|
||||
func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) Run(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver, uUID valuer.UUID)) *MockAlertmanager_UpdateChannelByReceiverAndID_Call {
|
||||
func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) Run(run func(context1 context.Context, s string, v alertmanagertypes.Receiver, uUID valuer.UUID)) *MockAlertmanager_UpdateChannelByReceiverAndID_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
@@ -1790,9 +1790,9 @@ func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) Run(run func(conte
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 *alertmanagertypes.Receiver
|
||||
var arg2 alertmanagertypes.Receiver
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(*alertmanagertypes.Receiver)
|
||||
arg2 = args[2].(alertmanagertypes.Receiver)
|
||||
}
|
||||
var arg3 valuer.UUID
|
||||
if args[3] != nil {
|
||||
@@ -1813,7 +1813,7 @@ func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) Return(err error)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) RunAndReturn(run func(context1 context.Context, s string, v *alertmanagertypes.Receiver, uUID valuer.UUID) error) *MockAlertmanager_UpdateChannelByReceiverAndID_Call {
|
||||
func (_c *MockAlertmanager_UpdateChannelByReceiverAndID_Call) RunAndReturn(run func(context1 context.Context, s string, v alertmanagertypes.Receiver, uUID valuer.UUID) error) *MockAlertmanager_UpdateChannelByReceiverAndID_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ func (service *Service) PutAlerts(ctx context.Context, orgID string, alerts aler
|
||||
return server.PutAlerts(ctx, alerts)
|
||||
}
|
||||
|
||||
func (service *Service) TestReceiver(ctx context.Context, orgID string, receiver *alertmanagertypes.Receiver) error {
|
||||
func (service *Service) TestReceiver(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
|
||||
service.serversMtx.RLock()
|
||||
defer service.serversMtx.RUnlock()
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ func (provider *provider) PutAlerts(ctx context.Context, orgID string, alerts al
|
||||
return provider.service.PutAlerts(ctx, orgID, alerts)
|
||||
}
|
||||
|
||||
func (provider *provider) TestReceiver(ctx context.Context, orgID string, receiver *alertmanagertypes.Receiver) error {
|
||||
func (provider *provider) TestReceiver(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) error {
|
||||
return provider.service.TestReceiver(ctx, orgID, receiver)
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ func (provider *provider) GetChannelByID(ctx context.Context, orgID string, chan
|
||||
return provider.configStore.GetChannelByID(ctx, orgID, channelID)
|
||||
}
|
||||
|
||||
func (provider *provider) UpdateChannelByReceiverAndID(ctx context.Context, orgID string, receiver *alertmanagertypes.Receiver, id valuer.UUID) error {
|
||||
func (provider *provider) UpdateChannelByReceiverAndID(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver, id valuer.UUID) error {
|
||||
channel, err := provider.configStore.GetChannelByID(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -211,7 +211,7 @@ func (provider *provider) DeleteChannelByID(ctx context.Context, orgID string, c
|
||||
}))
|
||||
}
|
||||
|
||||
func (provider *provider) CreateChannel(ctx context.Context, orgID string, receiver *alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error) {
|
||||
func (provider *provider) CreateChannel(ctx context.Context, orgID string, receiver alertmanagertypes.Receiver) (*alertmanagertypes.Channel, error) {
|
||||
config, err := provider.configStore.Get(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -212,26 +212,6 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}/services/{service_id}", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.GetAccountService),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetAccountService",
|
||||
Tags: []string{"cloudintegration"},
|
||||
Summary: "Get service for account",
|
||||
Description: "This endpoint gets a service and its configuration for the specified cloud integration account",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(citypes.Service),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Agent check-in endpoint is kept same as older one to maintain backward compatibility with already deployed agents.
|
||||
// In the future, this endpoint will be deprecated and a new endpoint will be introduced for consistency with above endpoints.
|
||||
if err := router.Handle("/api/v1/cloud-integrations/{cloud_provider}/agent-check-in", handler.New(
|
||||
|
||||
@@ -77,7 +77,6 @@ type Handler interface {
|
||||
ListServicesMetadata(http.ResponseWriter, *http.Request)
|
||||
ListAccountServicesMetadata(http.ResponseWriter, *http.Request)
|
||||
GetService(http.ResponseWriter, *http.Request)
|
||||
GetAccountService(http.ResponseWriter, *http.Request)
|
||||
UpdateService(http.ResponseWriter, *http.Request)
|
||||
AgentCheckIn(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
@@ -360,51 +360,6 @@ func (handler *handler) GetService(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Success(rw, http.StatusOK, svc)
|
||||
}
|
||||
|
||||
func (handler *handler) GetAccountService(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
provider, err := cloudintegrationtypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
serviceID, err := cloudintegrationtypes.NewServiceID(provider, mux.Vars(r)["service_id"])
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
cloudIntegrationID, err := valuer.NewUUID(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
_, err = handler.module.GetConnectedAccount(ctx, orgID, cloudIntegrationID, provider)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
svc, err := handler.module.GetService(ctx, orgID, serviceID, provider, cloudIntegrationID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, svc)
|
||||
}
|
||||
|
||||
func (handler *handler) UpdateService(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type addAlertmanager struct {
|
||||
@@ -247,38 +246,12 @@ func (migration *addAlertmanager) Down(ctx context.Context, db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// copy of alertmanagertypes.NewReceiver as it existed
|
||||
// when this migration was written.
|
||||
func newReceiver(input string) (config.Receiver, error) {
|
||||
receiver := config.Receiver{}
|
||||
err := json.Unmarshal([]byte(input), &receiver)
|
||||
if err != nil {
|
||||
return config.Receiver{}, err
|
||||
}
|
||||
|
||||
bytes, err := yaml.Marshal(receiver)
|
||||
if err != nil {
|
||||
return config.Receiver{}, err
|
||||
}
|
||||
|
||||
receiverWithDefaults := config.Receiver{}
|
||||
if err := yaml.Unmarshal(bytes, &receiverWithDefaults); err != nil {
|
||||
return config.Receiver{}, err
|
||||
}
|
||||
|
||||
if err := receiverWithDefaults.UnmarshalYAML(func(i interface{}) error { return nil }); err != nil {
|
||||
return config.Receiver{}, err
|
||||
}
|
||||
|
||||
return receiverWithDefaults, nil
|
||||
}
|
||||
|
||||
func (migration *addAlertmanager) msTeamsChannelToMSTeamsV2Channel(c *alertmanagertypes.Channel) error {
|
||||
if c.Type != "msteams" {
|
||||
return nil
|
||||
}
|
||||
|
||||
receiver, err := newReceiver(c.Data)
|
||||
receiver, err := alertmanagertypes.NewReceiver(c.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -296,7 +269,7 @@ func (migration *addAlertmanager) msTeamsChannelToMSTeamsV2Channel(c *alertmanag
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addAlertmanager) msTeamsReceiverToMSTeamsV2Receiver(receiver config.Receiver) config.Receiver {
|
||||
func (migration *addAlertmanager) msTeamsReceiverToMSTeamsV2Receiver(receiver alertmanagertypes.Receiver) alertmanagertypes.Receiver {
|
||||
if receiver.MSTeamsConfigs == nil {
|
||||
return receiver
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ func NewAlertsFromPostableAlerts(ctx context.Context, postableAlerts PostableAle
|
||||
return validAlerts, errs
|
||||
}
|
||||
|
||||
func NewTestAlert(receiver *Receiver, startsAt time.Time, updatedAt time.Time) *Alert {
|
||||
func NewTestAlert(receiver Receiver, startsAt time.Time, updatedAt time.Time) *Alert {
|
||||
return &Alert{
|
||||
Alert: model.Alert{
|
||||
StartsAt: startsAt,
|
||||
|
||||
@@ -56,7 +56,7 @@ type Channel struct {
|
||||
|
||||
// NewChannelFromReceiver creates a new Channel from a Receiver.
|
||||
// It can return nil if the receiver is the default receiver.
|
||||
func NewChannelFromReceiver(receiver *Receiver, orgID string) (*Channel, error) {
|
||||
func NewChannelFromReceiver(receiver config.Receiver, orgID string) (*Channel, error) {
|
||||
if receiver.Name == DefaultReceiverName {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeAlertmanagerChannelInvalid, "cannot use %s name as a channel name", receiver.Name)
|
||||
}
|
||||
@@ -74,56 +74,51 @@ func NewChannelFromReceiver(receiver *Receiver, orgID string) (*Channel, error)
|
||||
OrgID: orgID,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(receiver)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "marshal receiver")
|
||||
}
|
||||
channel.Data = string(data)
|
||||
// Use reflection to examine receiver struct fields
|
||||
receiverType := reflect.TypeOf(receiver)
|
||||
receiverVal := reflect.ValueOf(receiver)
|
||||
|
||||
channel.Type = receiverChannelType(receiver)
|
||||
if channel.Type == "" {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeAlertmanagerChannelInvalid, "channel '%s' must have at least one notification configuration (e.g., email_configs, webhook_configs, slack_configs)", receiver.Name)
|
||||
}
|
||||
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
// receiverChannelType returns the channel.Type discriminator. Walks
|
||||
// Receiver's own fields first (native), then the embed (upstream); first
|
||||
// non-empty *_configs slice wins.
|
||||
func receiverChannelType(receiver *Receiver) string {
|
||||
if t := nonEmptyConfigsField(reflect.ValueOf(*receiver)); t != "" {
|
||||
return t
|
||||
}
|
||||
if t := nonEmptyConfigsField(reflect.ValueOf(*receiver.Receiver)); t != "" {
|
||||
return t
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func nonEmptyConfigsField(v reflect.Value) string {
|
||||
t := v.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
fieldVal := v.Field(i)
|
||||
// Iterate through fields looking for *Config fields
|
||||
for i := 0; i < receiverType.NumField(); i++ {
|
||||
field := receiverType.Field(i)
|
||||
fieldVal := receiverVal.Field(i)
|
||||
|
||||
// Skip if not a slice or is empty
|
||||
if fieldVal.Kind() != reflect.Slice || fieldVal.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get channel type from yaml tag
|
||||
yamlTag := field.Tag.Get("yaml")
|
||||
if yamlTag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract the base type name (e.g., "email_configs" -> "email").
|
||||
// Extract the base type name (e.g., "email_configs" -> "email")
|
||||
matches := receiverTypeRegex.FindStringSubmatch(yamlTag)
|
||||
if len(matches) != 2 {
|
||||
continue
|
||||
}
|
||||
return matches[1]
|
||||
|
||||
channelType := matches[1]
|
||||
|
||||
// Marshal config data to JSON
|
||||
configData, err := json.Marshal(receiver)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
channel.Type = channelType
|
||||
channel.Data = string(configData)
|
||||
break
|
||||
}
|
||||
return ""
|
||||
|
||||
// If we were unable to find the channel type, return an error
|
||||
if channel.Type == "" {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeAlertmanagerChannelInvalid, "channel '%s' must have at least one notification configuration (e.g., email_configs, webhook_configs, slack_configs)", receiver.Name)
|
||||
}
|
||||
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
func NewConfigFromChannels(globalConfig GlobalConfig, routeConfig RouteConfig, channels Channels, orgID string) (*Config, error) {
|
||||
@@ -187,7 +182,7 @@ func NewStatsFromChannels(channels Channels) map[string]any {
|
||||
return stats
|
||||
}
|
||||
|
||||
func (c *Channel) Update(receiver *Receiver) error {
|
||||
func (c *Channel) Update(receiver Receiver) error {
|
||||
channel, err := NewChannelFromReceiver(receiver, c.OrgID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -197,7 +192,6 @@ func (c *Channel) Update(receiver *Receiver) error {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAlertmanagerChannelNameMismatch, "cannot update channel name")
|
||||
}
|
||||
|
||||
c.Type = channel.Type
|
||||
c.Data = channel.Data
|
||||
c.UpdatedAt = time.Now()
|
||||
|
||||
@@ -216,19 +210,15 @@ func (PostableChannel) JSONSchema() (jsonschema.Schema, error) {
|
||||
schema.WithRequired("name")
|
||||
|
||||
var oneOf []jsonschema.SchemaOrBool
|
||||
// Walk both halves: native fields on Receiver, upstream on the embed.
|
||||
collect := func(t reflect.Type) {
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
jsonTag := strings.Split(t.Field(i).Tag.Get("json"), ",")[0]
|
||||
if !strings.HasSuffix(jsonTag, "_configs") {
|
||||
continue
|
||||
}
|
||||
branch := (&jsonschema.Schema{}).WithRequired(jsonTag)
|
||||
oneOf = append(oneOf, branch.ToSchemaOrBool())
|
||||
receiverType := reflect.TypeOf(Receiver{})
|
||||
for i := 0; i < receiverType.NumField(); i++ {
|
||||
jsonTag := strings.Split(receiverType.Field(i).Tag.Get("json"), ",")[0]
|
||||
if !strings.HasSuffix(jsonTag, "_configs") {
|
||||
continue
|
||||
}
|
||||
branch := (&jsonschema.Schema{}).WithRequired(jsonTag)
|
||||
oneOf = append(oneOf, branch.ToSchemaOrBool())
|
||||
}
|
||||
collect(reflect.TypeOf(Receiver{}))
|
||||
collect(reflect.TypeOf(config.Receiver{}))
|
||||
|
||||
schema.WithOneOf(oneOf...)
|
||||
|
||||
|
||||
@@ -285,8 +285,7 @@ func TestNewChannelFromReceiver(t *testing.T) {
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
receiver := testCase.receiver
|
||||
channel, err := NewChannelFromReceiver(&Receiver{Receiver: &receiver}, "1")
|
||||
channel, err := NewChannelFromReceiver(testCase.receiver, "1")
|
||||
if !testCase.pass {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
@@ -300,31 +299,3 @@ func TestNewChannelFromReceiver(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Type and Data are derived from the native googlechat_configs field.
|
||||
func TestNewChannelFromReceiverGoogleChat(t *testing.T) {
|
||||
webhookURL, err := url.Parse("https://chat.googleapis.com/v1/spaces/test/messages")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
receiver := &Receiver{
|
||||
Receiver: &config.Receiver{Name: "googlechat-receiver"},
|
||||
GoogleChatConfigs: []*GoogleChatReceiverConfig{
|
||||
{
|
||||
WebhookURL: &config.SecretURL{URL: webhookURL},
|
||||
Title: "Alert",
|
||||
Text: "Body",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
channel, err := NewChannelFromReceiver(receiver, "1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "googlechat-receiver", channel.Name)
|
||||
assert.Equal(t, "googlechat", channel.Type)
|
||||
assert.JSONEq(t,
|
||||
`{"name":"googlechat-receiver","googlechat_configs":[{"send_resolved":false,"webhook_url":"https://chat.googleapis.com/v1/spaces/test/messages","title":"Alert","text":"Body"}]}`,
|
||||
channel.Data,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -59,43 +59,12 @@ type Config struct {
|
||||
|
||||
// storeableConfig is the representation of the config in the store
|
||||
storeableConfig *StoreableConfig
|
||||
|
||||
// customConfigs holds the custom notifier configs upstream's
|
||||
// config.Receiver cannot carry, keyed by receiver name.
|
||||
customConfigs map[string]customReceiverConfigs
|
||||
}
|
||||
|
||||
// customReceiverConfigs is the per-receiver custom notifier
|
||||
// configs. To add another, mirror GoogleChat: a field here, a matching field
|
||||
// on Receiver, and extensions to customConfigsOf + isEmpty.
|
||||
type customReceiverConfigs struct {
|
||||
GoogleChat []*GoogleChatReceiverConfig
|
||||
}
|
||||
|
||||
func (c customReceiverConfigs) isEmpty() bool {
|
||||
return len(c.GoogleChat) == 0
|
||||
}
|
||||
|
||||
func customConfigsOf(receiver *Receiver) customReceiverConfigs {
|
||||
return customReceiverConfigs{
|
||||
GoogleChat: receiver.GoogleChatConfigs,
|
||||
}
|
||||
}
|
||||
|
||||
// storedConfig is the persistence unit. The outer Receivers shadows the
|
||||
// embed's so receivers emit as the extended *Receiver (encoding/json:
|
||||
// shallower-field-wins on duplicate JSON names).
|
||||
type storedConfig struct {
|
||||
*config.Config
|
||||
Receivers []*Receiver `json:"receivers"`
|
||||
}
|
||||
|
||||
func NewConfig(c *config.Config, orgID string) *Config {
|
||||
customConfigs := make(map[string]customReceiverConfigs)
|
||||
raw := string(newRawFromConfig(c, customConfigs))
|
||||
raw := string(newRawFromConfig(c))
|
||||
return &Config{
|
||||
alertmanagerConfig: c,
|
||||
customConfigs: customConfigs,
|
||||
storeableConfig: &StoreableConfig{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
@@ -112,14 +81,13 @@ func NewConfig(c *config.Config, orgID string) *Config {
|
||||
}
|
||||
|
||||
func NewConfigFromStoreableConfig(sc *StoreableConfig) (*Config, error) {
|
||||
alertmanagerConfig, customConfigs, err := newConfigFromString(sc.Config)
|
||||
alertmanagerConfig, err := newConfigFromString(sc.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Config{
|
||||
alertmanagerConfig: alertmanagerConfig,
|
||||
customConfigs: customConfigs,
|
||||
storeableConfig: sc,
|
||||
}, nil
|
||||
}
|
||||
@@ -145,47 +113,32 @@ func NewDefaultConfig(globalConfig GlobalConfig, routeConfig RouteConfig, orgID
|
||||
}, orgID), nil
|
||||
}
|
||||
|
||||
func newConfigFromString(s string) (*config.Config, map[string]customReceiverConfigs, error) {
|
||||
stored := storedConfig{Config: new(config.Config)}
|
||||
if err := json.Unmarshal([]byte(s), &stored); err != nil {
|
||||
return nil, nil, err
|
||||
func newConfigFromString(s string) (*config.Config, error) {
|
||||
config := new(config.Config)
|
||||
err := json.Unmarshal([]byte(s), config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
amConfig := stored.Config
|
||||
amConfig.Receivers = make([]config.Receiver, len(stored.Receivers))
|
||||
customConfigs := make(map[string]customReceiverConfigs)
|
||||
for i, receiver := range config.Receivers {
|
||||
bytes, err := json.Marshal(receiver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Re-run NewReceiver per receiver so defaults apply (mirrors create path).
|
||||
for i, rcv := range stored.Receivers {
|
||||
rcvJSON, err := json.Marshal(rcv)
|
||||
receiver, err := NewReceiver(string(bytes))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
parsed, err := NewReceiver(string(rcvJSON))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
amConfig.Receivers[i] = *parsed.Receiver
|
||||
if custom := customConfigsOf(parsed); !custom.isEmpty() {
|
||||
customConfigs[parsed.Name] = custom
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.Receivers[i] = receiver
|
||||
}
|
||||
|
||||
return amConfig, customConfigs, nil
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func newRawFromConfig(c *config.Config, customConfigs map[string]customReceiverConfigs) []byte {
|
||||
receivers := make([]*Receiver, len(c.Receivers))
|
||||
for i := range c.Receivers {
|
||||
base := c.Receivers[i]
|
||||
custom := customConfigs[base.Name]
|
||||
receivers[i] = &Receiver{
|
||||
Receiver: &base,
|
||||
GoogleChatConfigs: custom.GoogleChat,
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.Marshal(storedConfig{Config: c, Receivers: receivers})
|
||||
func newRawFromConfig(c *config.Config) []byte {
|
||||
b, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
// Taking inspiration from the upstream. This is never expected to happen.
|
||||
return []byte(fmt.Sprintf("<error creating config string: %s>", err))
|
||||
@@ -198,14 +151,6 @@ func newConfigHash(s string) [16]byte {
|
||||
return md5.Sum([]byte(s))
|
||||
}
|
||||
|
||||
// flush refreshes the storeable representation. Call after any mutation.
|
||||
func (c *Config) flush() {
|
||||
raw := string(newRawFromConfig(c.alertmanagerConfig, c.customConfigs))
|
||||
c.storeableConfig.Config = raw
|
||||
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(raw))
|
||||
c.storeableConfig.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
func (c *Config) CopyWithReset() (*Config, error) {
|
||||
newConfig, err := NewDefaultConfig(
|
||||
*c.alertmanagerConfig.Global,
|
||||
@@ -234,7 +179,9 @@ func (c *Config) SetGlobalConfig(globalConfig GlobalConfig) error {
|
||||
globalConfig.SMTPRequireTLS = smtpRequireTLS
|
||||
|
||||
c.alertmanagerConfig.Global = &globalConfig
|
||||
c.flush()
|
||||
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||
c.storeableConfig.UpdatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -246,7 +193,9 @@ func (c *Config) SetRouteConfig(routeConfig RouteConfig) error {
|
||||
}
|
||||
c.alertmanagerConfig.Route = route
|
||||
|
||||
c.flush()
|
||||
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||
c.storeableConfig.UpdatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -258,7 +207,9 @@ func (c *Config) AddInhibitRules(rules []config.InhibitRule) error {
|
||||
|
||||
c.alertmanagerConfig.InhibitRules = append(c.alertmanagerConfig.InhibitRules, rules...)
|
||||
|
||||
c.flush()
|
||||
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||
c.storeableConfig.UpdatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -271,7 +222,7 @@ func (c *Config) StoreableConfig() *StoreableConfig {
|
||||
return c.storeableConfig
|
||||
}
|
||||
|
||||
func (c *Config) CreateReceiver(receiver *Receiver) error {
|
||||
func (c *Config) CreateReceiver(receiver config.Receiver) error {
|
||||
// check that receiver name is not already used
|
||||
for _, existingReceiver := range c.alertmanagerConfig.Receivers {
|
||||
if existingReceiver.Name == receiver.Name {
|
||||
@@ -285,39 +236,33 @@ func (c *Config) CreateReceiver(receiver *Receiver) error {
|
||||
}
|
||||
|
||||
c.alertmanagerConfig.Route.Routes = append(c.alertmanagerConfig.Route.Routes, route)
|
||||
c.alertmanagerConfig.Receivers = append(c.alertmanagerConfig.Receivers, *receiver.Receiver)
|
||||
c.setCustomConfigs(receiver)
|
||||
c.alertmanagerConfig.Receivers = append(c.alertmanagerConfig.Receivers, receiver)
|
||||
|
||||
if err := c.alertmanagerConfig.UnmarshalYAML(func(i interface{}) error { return nil }); err != nil {
|
||||
return err
|
||||
}
|
||||
c.applyNativeDefaults()
|
||||
|
||||
c.flush()
|
||||
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||
c.storeableConfig.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) GetReceiver(name string) (*Receiver, error) {
|
||||
for i := range c.alertmanagerConfig.Receivers {
|
||||
if c.alertmanagerConfig.Receivers[i].Name == name {
|
||||
base := c.alertmanagerConfig.Receivers[i]
|
||||
custom := c.customConfigs[name]
|
||||
return &Receiver{
|
||||
Receiver: &base,
|
||||
GoogleChatConfigs: custom.GoogleChat,
|
||||
}, nil
|
||||
func (c *Config) GetReceiver(name string) (Receiver, error) {
|
||||
for _, receiver := range c.alertmanagerConfig.Receivers {
|
||||
if receiver.Name == name {
|
||||
return receiver, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.Newf(errors.TypeNotFound, ErrCodeAlertmanagerChannelNotFound, "channel with name %q not found", name)
|
||||
return Receiver{}, errors.Newf(errors.TypeNotFound, ErrCodeAlertmanagerChannelNotFound, "channel with name %q not found", name)
|
||||
}
|
||||
|
||||
func (c *Config) UpdateReceiver(receiver *Receiver) error {
|
||||
func (c *Config) UpdateReceiver(receiver config.Receiver) error {
|
||||
// find and update receiver
|
||||
for i, existingReceiver := range c.alertmanagerConfig.Receivers {
|
||||
if existingReceiver.Name == receiver.Name {
|
||||
c.alertmanagerConfig.Receivers[i] = *receiver.Receiver
|
||||
c.setCustomConfigs(receiver)
|
||||
c.alertmanagerConfig.Receivers[i] = receiver
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -325,9 +270,10 @@ func (c *Config) UpdateReceiver(receiver *Receiver) error {
|
||||
if err := c.alertmanagerConfig.UnmarshalYAML(func(i interface{}) error { return nil }); err != nil {
|
||||
return err
|
||||
}
|
||||
c.applyNativeDefaults()
|
||||
|
||||
c.flush()
|
||||
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||
c.storeableConfig.UpdatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -352,36 +298,13 @@ func (c *Config) DeleteReceiver(name string) error {
|
||||
}
|
||||
}
|
||||
|
||||
delete(c.customConfigs, name)
|
||||
|
||||
c.flush()
|
||||
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||
c.storeableConfig.UpdatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) setCustomConfigs(receiver *Receiver) {
|
||||
if custom := customConfigsOf(receiver); !custom.isEmpty() {
|
||||
c.customConfigs[receiver.Name] = custom
|
||||
} else {
|
||||
delete(c.customConfigs, receiver.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) applyNativeDefaults() {
|
||||
if c.alertmanagerConfig.Global == nil {
|
||||
return
|
||||
}
|
||||
httpDefault := c.alertmanagerConfig.Global.HTTPConfig
|
||||
|
||||
for _, custom := range c.customConfigs {
|
||||
for _, gc := range custom.GoogleChat {
|
||||
if gc.HTTPConfig == nil {
|
||||
gc.HTTPConfig = httpDefault
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) CreateRuleIDMatcher(ruleID string, receiverNames []string) error {
|
||||
if c.alertmanagerConfig.Route == nil {
|
||||
return errors.New(errors.TypeInvalidInput, ErrCodeAlertmanagerConfigInvalid, "route is nil")
|
||||
@@ -395,7 +318,9 @@ func (c *Config) CreateRuleIDMatcher(ruleID string, receiverNames []string) erro
|
||||
}
|
||||
}
|
||||
|
||||
c.flush()
|
||||
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||
c.storeableConfig.UpdatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -414,7 +339,9 @@ func (c *Config) DeleteRuleIDInhibitor(ruleID string) error {
|
||||
}
|
||||
}
|
||||
c.alertmanagerConfig.InhibitRules = filteredRules
|
||||
c.flush()
|
||||
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||
c.storeableConfig.UpdatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -435,7 +362,9 @@ func (c *Config) DeleteRuleIDMatcher(ruleID string) error {
|
||||
}
|
||||
}
|
||||
|
||||
c.flush()
|
||||
c.storeableConfig.Config = string(newRawFromConfig(c.alertmanagerConfig))
|
||||
c.storeableConfig.Hash = fmt.Sprintf("%x", newConfigHash(c.storeableConfig.Config))
|
||||
c.storeableConfig.UpdatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user