mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-22 09:50:25 +01:00
Compare commits
336 Commits
ns/flamegr
...
feature/da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ae33fc8bb | ||
|
|
00d0cf8920 | ||
|
|
448528f0e8 | ||
|
|
abeab11271 | ||
|
|
26b007cce5 | ||
|
|
0d1766b7c2 | ||
|
|
61c15c704b | ||
|
|
6702930688 | ||
|
|
4a24ee44e8 | ||
|
|
0045ecadab | ||
|
|
69aad53eb4 | ||
|
|
7bacb03483 | ||
|
|
fb3e316ce9 | ||
|
|
b753b95a8a | ||
|
|
4757550189 | ||
|
|
96ad37fea9 | ||
|
|
5419e8461c | ||
|
|
e634eb4452 | ||
|
|
a50bc53f4c | ||
|
|
9f60bdf54a | ||
|
|
e41639dea0 | ||
|
|
847bc71f4e | ||
|
|
8d7d3e5c64 | ||
|
|
74c875ec79 | ||
|
|
a27b7d3d8e | ||
|
|
eab8d45611 | ||
|
|
c73fdd1f81 | ||
|
|
6094a5eee2 | ||
|
|
93407d3bcc | ||
|
|
5bd4cabbca | ||
|
|
f9e21cecd8 | ||
|
|
4b98b0bb27 | ||
|
|
b48851e487 | ||
|
|
279a71c5b3 | ||
|
|
7e63e35113 | ||
|
|
d5a50fe456 | ||
|
|
885b41356a | ||
|
|
b610056954 | ||
|
|
db77b398e7 | ||
|
|
574867bafb | ||
|
|
d87edca9d1 | ||
|
|
b653c69e29 | ||
|
|
7d2f8b291e | ||
|
|
3bea4484f9 | ||
|
|
87ceba2d84 | ||
|
|
445dc3b290 | ||
|
|
76b35b9d8f | ||
|
|
b860cce31d | ||
|
|
1bd4ca88de | ||
|
|
a7debaa6ed | ||
|
|
85ac805fae | ||
|
|
8a0441293a | ||
|
|
25ae787ecb | ||
|
|
f0ed0a8967 | ||
|
|
b47343bc09 | ||
|
|
bb39c52229 | ||
|
|
5fe69473c9 | ||
|
|
9be77ace42 | ||
|
|
c804d8f9b6 | ||
|
|
996bd949f2 | ||
|
|
84225023a5 | ||
|
|
a5b9dd279c | ||
|
|
172418a337 | ||
|
|
d1d5a9fa32 | ||
|
|
6f81e9f364 | ||
|
|
1933bec786 | ||
|
|
6d3d9bfb49 | ||
|
|
8b89f4af85 | ||
|
|
66e4132504 | ||
|
|
29e14ce9c6 | ||
|
|
d7dc789a58 | ||
|
|
d2d129eea9 | ||
|
|
9e1704615f | ||
|
|
db06557c12 | ||
|
|
1475e2b53a | ||
|
|
e58119a416 | ||
|
|
937a469e80 | ||
|
|
b0262a7d89 | ||
|
|
18200c049e | ||
|
|
3dc5b53cc3 | ||
|
|
60fef93100 | ||
|
|
f5935ccaf4 | ||
|
|
d354044fbe | ||
|
|
fe6cbc3c0c | ||
|
|
45fa0c739c | ||
|
|
e1527dd148 | ||
|
|
5063be6467 | ||
|
|
b453655dea | ||
|
|
8e4521177d | ||
|
|
edf2c4493b | ||
|
|
39174d6040 | ||
|
|
2e7500a0b2 | ||
|
|
5987228e4a | ||
|
|
08a59145cc | ||
|
|
744856680e | ||
|
|
681e205ac1 | ||
|
|
4cb27e330e | ||
|
|
b47bc6bcc4 | ||
|
|
5321e9ee87 | ||
|
|
dd615869f6 | ||
|
|
fc4f326953 | ||
|
|
2af4cbf0f9 | ||
|
|
e82f568a27 | ||
|
|
c2aac3a278 | ||
|
|
ddfec3e5f7 | ||
|
|
af623f66e8 | ||
|
|
1cdecceece | ||
|
|
fb4f0a9c63 | ||
|
|
268e747f5c | ||
|
|
3fc72329c9 | ||
|
|
10ecb7524c | ||
|
|
8fc21ca6b9 | ||
|
|
422149369d | ||
|
|
2063697350 | ||
|
|
685faa9211 | ||
|
|
de6fcb9fbb | ||
|
|
828619a9e6 | ||
|
|
aff2e1be6b | ||
|
|
9728d17a0a | ||
|
|
ffaf334dfd | ||
|
|
d87e7241c0 | ||
|
|
ae184315a9 | ||
|
|
29f782a3a0 | ||
|
|
71d8dafce1 | ||
|
|
412320d7d9 | ||
|
|
9b5d78b5a0 | ||
|
|
444464ae15 | ||
|
|
d5841f8daa | ||
|
|
c344cd256f | ||
|
|
563b289469 | ||
|
|
eae3ff7ee6 | ||
|
|
f228f2c9bf | ||
|
|
44afbbe122 | ||
|
|
771ae521ab | ||
|
|
7b97979d84 | ||
|
|
4c604c3079 | ||
|
|
a11bc8c325 | ||
|
|
35930198d0 | ||
|
|
48f4838b93 | ||
|
|
ce7735d348 | ||
|
|
2f6b7b6260 | ||
|
|
5e61be1606 | ||
|
|
124b529392 | ||
|
|
27f334dbe0 | ||
|
|
b626d4b868 | ||
|
|
3019d151ae | ||
|
|
7fa00ef30b | ||
|
|
1abf66f593 | ||
|
|
d8f7e62565 | ||
|
|
9380569223 | ||
|
|
1605b1c1ec | ||
|
|
42660ca8a6 | ||
|
|
1f7032953c | ||
|
|
173037d3be | ||
|
|
e9aab5a618 | ||
|
|
c0113324ca | ||
|
|
6d59fa4700 | ||
|
|
4713fd4839 | ||
|
|
4ad872b722 | ||
|
|
642fb66831 | ||
|
|
d12c846212 | ||
|
|
4e5bd7cf6f | ||
|
|
3982cce603 | ||
|
|
1a43c85cb8 | ||
|
|
bd11e985e1 | ||
|
|
4f82bae07a | ||
|
|
aa6066f7a8 | ||
|
|
128abf413e | ||
|
|
abd7e41f97 | ||
|
|
3ebde75ebd | ||
|
|
3e849ee2d3 | ||
|
|
f7d9a57637 | ||
|
|
629779a666 | ||
|
|
c864faf01f | ||
|
|
99802daa3d | ||
|
|
7dfa474dc1 | ||
|
|
5c223e9b04 | ||
|
|
fceb770337 | ||
|
|
44496d9d8d | ||
|
|
24d3f65200 | ||
|
|
9ceaaeecf1 | ||
|
|
3e9c1fd7c9 | ||
|
|
3b0fa192d8 | ||
|
|
8c44c42e13 | ||
|
|
398943fe41 | ||
|
|
a17debc61b | ||
|
|
c13270814a | ||
|
|
7eb6dbe4a6 | ||
|
|
ce424b776b | ||
|
|
0fae729715 | ||
|
|
6079e9869c | ||
|
|
3113b82904 | ||
|
|
71c60c3f2a | ||
|
|
abfc19e27a | ||
|
|
762a852a4f | ||
|
|
3cc2a689c8 | ||
|
|
b74f5854fc | ||
|
|
3b824d50a3 | ||
|
|
d0a693b034 | ||
|
|
ee4508cb85 | ||
|
|
b2aec2edaf | ||
|
|
cd7899795d | ||
|
|
ad2d1467ec | ||
|
|
90377f8116 | ||
|
|
cabfd7271b | ||
|
|
750d63cf6b | ||
|
|
d0bfee2645 | ||
|
|
8b505c0197 | ||
|
|
431fb7ca62 | ||
|
|
44cf8ed8e7 | ||
|
|
4d1129c85f | ||
|
|
e4c4acb5df | ||
|
|
c9235cd3d2 | ||
|
|
ec837c7006 | ||
|
|
cbba2e16d8 | ||
|
|
1d0ab788d5 | ||
|
|
4aae71462b | ||
|
|
bd49d94144 | ||
|
|
fa7205a673 | ||
|
|
13ec049495 | ||
|
|
3e8468ab23 | ||
|
|
e6cb7fabde | ||
|
|
59b8fa0e05 | ||
|
|
133a3a0057 | ||
|
|
b4e524dae0 | ||
|
|
2aa46f9f86 | ||
|
|
73fa15da83 | ||
|
|
cd70d0bdeb | ||
|
|
4de0092664 | ||
|
|
337d23c91f | ||
|
|
a1f73655ca | ||
|
|
0d6081d0d0 | ||
|
|
301d0103b0 | ||
|
|
dc99772ee4 | ||
|
|
80849ebfeb | ||
|
|
2c0c7240a4 | ||
|
|
28cb0a8be7 | ||
|
|
54832cad34 | ||
|
|
a45178d709 | ||
|
|
c4224ecf72 | ||
|
|
14927c89d3 | ||
|
|
55487dde3a | ||
|
|
fc5717af51 | ||
|
|
8bf650192e | ||
|
|
f8fb7e5f8d | ||
|
|
ff578f7d92 | ||
|
|
cd630b1152 | ||
|
|
bd0842ac17 | ||
|
|
b3e3dd13b4 | ||
|
|
710d5531f3 | ||
|
|
e37e427079 | ||
|
|
1e99ab4659 | ||
|
|
3353cda021 | ||
|
|
f5a71037bf | ||
|
|
97b85c386a | ||
|
|
00bdf50c1c | ||
|
|
5dec4ec580 | ||
|
|
325767c240 | ||
|
|
5fed2a4585 | ||
|
|
664337ae0f | ||
|
|
a0ea276681 | ||
|
|
2dc8699f08 | ||
|
|
ed81ed8ab5 | ||
|
|
48c9da19df | ||
|
|
eb9663d518 | ||
|
|
a56a862338 | ||
|
|
021f33f65e | ||
|
|
ca96c71146 | ||
|
|
de2909d1d1 | ||
|
|
f311fcabf7 | ||
|
|
a37c07f881 | ||
|
|
4d9386f418 | ||
|
|
737473521d | ||
|
|
1863db8ba8 | ||
|
|
661af09a13 | ||
|
|
6024fa2b91 | ||
|
|
8996a96387 | ||
|
|
d6db5c2aab | ||
|
|
709590ea1b | ||
|
|
1add46b4c5 | ||
|
|
8401261e20 | ||
|
|
0ff34a7274 | ||
|
|
44e3bd9608 | ||
|
|
c3944d779e | ||
|
|
f5ec783a53 | ||
|
|
35b729c425 | ||
|
|
4f43c3d803 | ||
|
|
5dbde6c64d | ||
|
|
fb6fdd54ec | ||
|
|
64b8ba62da | ||
|
|
7c66df408b | ||
|
|
54049de391 | ||
|
|
a82f4237c8 | ||
|
|
89606b6238 | ||
|
|
db5ce958eb | ||
|
|
c8d3a9a54b | ||
|
|
637870b1fc | ||
|
|
d46a7e24c9 | ||
|
|
2a451e1c31 | ||
|
|
60b6d1d890 | ||
|
|
36f755b232 | ||
|
|
c1b3e3683a | ||
|
|
4c68544b1a | ||
|
|
90d9ab95f9 | ||
|
|
065e712e0c | ||
|
|
50db309ecd | ||
|
|
261bc552b0 | ||
|
|
bab720e98b | ||
|
|
71fef6636b | ||
|
|
fc3cdecbbb | ||
|
|
860fcfa641 | ||
|
|
a090e3a4aa | ||
|
|
6cf73e2ade | ||
|
|
bbcb6a45d6 | ||
|
|
d13934febc | ||
|
|
d5a7b7523d | ||
|
|
5b8984f131 | ||
|
|
6ddc5f1f12 | ||
|
|
055968bfad | ||
|
|
1bf0f38ed9 | ||
|
|
842125e20a | ||
|
|
6dab35caf8 | ||
|
|
047e9e2001 | ||
|
|
45eaa7db58 | ||
|
|
8a3d894eba | ||
|
|
5239060b53 | ||
|
|
42c6f507ac | ||
|
|
1b695a0b80 | ||
|
|
438cfab155 | ||
|
|
69f7617e01 | ||
|
|
4420a7e1fc | ||
|
|
b4bc68c5c5 | ||
|
|
eb9eb317cc | ||
|
|
0b1eb16a42 | ||
|
|
05a4d12183 | ||
|
|
bbaf64c4f0 |
@@ -8,6 +8,14 @@ packages:
|
||||
filename: "alertmanager.go"
|
||||
structname: 'Mock{{.InterfaceName}}'
|
||||
pkgname: '{{.SrcPackageName}}test'
|
||||
github.com/SigNoz/signoz/pkg/types/alertmanagertypes:
|
||||
interfaces:
|
||||
MaintenanceStore:
|
||||
config:
|
||||
dir: '{{.InterfaceDir}}/alertmanagertypestest'
|
||||
filename: "maintenance.go"
|
||||
structname: 'Mock{{.InterfaceName}}'
|
||||
pkgname: '{{.SrcPackageName}}test'
|
||||
github.com/SigNoz/signoz/pkg/tokenizer:
|
||||
config:
|
||||
all: true
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tag"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
@@ -100,8 +101,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, authtypes.NewRegistry()), nil
|
||||
},
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
|
||||
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing, tagModule tag.Module) dashboard.Module {
|
||||
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, tagModule)
|
||||
},
|
||||
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
|
||||
return noopgateway.NewProviderFactory()
|
||||
|
||||
@@ -46,6 +46,7 @@ import (
|
||||
pkgcloudintegration "github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tag"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
@@ -133,8 +134,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
}
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, authtypes.NewRegistry()), nil
|
||||
},
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
|
||||
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
|
||||
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), store, settings, analytics, orgGetter, queryParser, querier, licensing, tagModule)
|
||||
},
|
||||
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
|
||||
return httpgateway.NewProviderFactory(licensing)
|
||||
|
||||
@@ -190,7 +190,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.124.0
|
||||
image: signoz/signoz:v0.125.1
|
||||
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.124.0
|
||||
image: signoz/signoz:v0.125.1
|
||||
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.124.0}
|
||||
image: signoz/signoz:${VERSION:-v0.125.1}
|
||||
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.124.0}
|
||||
image: signoz/signoz:${VERSION:-v0.125.1}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
1652
docs/api/openapi.yml
1652
docs/api/openapi.yml
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,10 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tag"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
@@ -30,9 +32,9 @@ type module struct {
|
||||
licensing licensing.Licensing
|
||||
}
|
||||
|
||||
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
|
||||
func NewModule(store dashboardtypes.Store, sqlstore sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
|
||||
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard")
|
||||
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser)
|
||||
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser, tagModule)
|
||||
|
||||
return &module{
|
||||
pkgDashboardModule: pkgDashboardModule,
|
||||
@@ -49,6 +51,14 @@ func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publi
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
dashboard, err := module.Get(ctx, orgID, publicDashboard.DashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dashboard.ErrIfNotPublishable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storablePublicDashboard, err := module.store.GetPublic(ctx, publicDashboard.DashboardID.StringValue())
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
@@ -129,6 +139,14 @@ func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publi
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
dashboard, err := module.Get(ctx, orgID, publicDashboard.DashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dashboard.ErrIfNotPublishable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.UpdatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(publicDashboard))
|
||||
}
|
||||
|
||||
@@ -138,6 +156,10 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
|
||||
return err
|
||||
}
|
||||
|
||||
if err := dashboard.ErrIfNotDeletable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dashboard.Locked {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
|
||||
}
|
||||
@@ -168,6 +190,14 @@ func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashb
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
dashboard, err := module.Get(ctx, orgID, dashboardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dashboard.ErrIfNotPublishable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = module.store.DeletePublic(ctx, dashboardID.StringValue())
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -197,6 +227,38 @@ func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy s
|
||||
return module.pkgDashboardModule.Create(ctx, orgID, createdBy, creator, data)
|
||||
}
|
||||
|
||||
func (module *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, postable dashboardtypes.PostableDashboardV2) (*dashboardtypes.DashboardV2, error) {
|
||||
return module.pkgDashboardModule.CreateV2(ctx, orgID, createdBy, creator, postable)
|
||||
}
|
||||
|
||||
func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
|
||||
return module.pkgDashboardModule.GetV2(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updateable dashboardtypes.UpdateableDashboardV2) (*dashboardtypes.DashboardV2, error) {
|
||||
return module.pkgDashboardModule.UpdateV2(ctx, orgID, id, updatedBy, updateable)
|
||||
}
|
||||
|
||||
func (module *module) PatchV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, patch dashboardtypes.PatchableDashboardV2) (*dashboardtypes.DashboardV2, error) {
|
||||
return module.pkgDashboardModule.PatchV2(ctx, orgID, id, updatedBy, patch)
|
||||
}
|
||||
|
||||
func (module *module) LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
|
||||
return module.pkgDashboardModule.LockUnlockV2(ctx, orgID, id, updatedBy, isAdmin, lock)
|
||||
}
|
||||
|
||||
func (module *module) ListV2(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, params *dashboardtypes.ListDashboardsV2Params) (*dashboardtypes.ListableDashboardV2, error) {
|
||||
return module.pkgDashboardModule.ListV2(ctx, orgID, userID, params)
|
||||
}
|
||||
|
||||
func (module *module) PinV2(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, id valuer.UUID) error {
|
||||
return module.pkgDashboardModule.PinV2(ctx, orgID, userID, id)
|
||||
}
|
||||
|
||||
func (module *module) UnpinV2(ctx context.Context, userID valuer.UUID, id valuer.UUID) error {
|
||||
return module.pkgDashboardModule.UnpinV2(ctx, userID, id)
|
||||
}
|
||||
|
||||
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
|
||||
return module.pkgDashboardModule.Get(ctx, orgID, id)
|
||||
}
|
||||
|
||||
@@ -80,6 +80,15 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
Route: "",
|
||||
})
|
||||
|
||||
fineGrainedAuthz := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseFineGrainedAuthz, evalCtx)
|
||||
featureSet = append(featureSet, &licensetypes.Feature{
|
||||
Name: valuer.NewString(flagger.FeatureUseFineGrainedAuthz.String()),
|
||||
Active: fineGrainedAuthz,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
})
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.DotMetricsEnabled {
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) {
|
||||
@@ -49,7 +48,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
rules = append(rules, tr)
|
||||
|
||||
// create ch rule task for evaluation
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc)
|
||||
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
|
||||
|
||||
@@ -73,7 +72,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
rules = append(rules, pr)
|
||||
|
||||
// create promql rule task for evaluation
|
||||
task = newTask(baserules.TaskTypeProm, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
task = newTask(baserules.TaskTypeProm, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc)
|
||||
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeAnomaly {
|
||||
// create anomaly rule
|
||||
@@ -96,7 +95,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
rules = append(rules, ar)
|
||||
|
||||
// create anomaly rule task for evaluation
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc)
|
||||
|
||||
} else {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
|
||||
@@ -210,9 +209,9 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
|
||||
}
|
||||
|
||||
// newTask returns an appropriate group for the rule type
|
||||
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, maintenanceStore ruletypes.MaintenanceStore, orgID valuer.UUID) baserules.Task {
|
||||
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc) baserules.Task {
|
||||
if taskType == baserules.TaskTypeCh {
|
||||
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
|
||||
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify)
|
||||
}
|
||||
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
|
||||
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify)
|
||||
}
|
||||
|
||||
65
ee/sqlstore/postgressqlstore/listfilter_test.go
Normal file
65
ee/sqlstore/postgressqlstore/listfilter_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package postgressqlstore
|
||||
|
||||
// Lives in this package (rather than the listfilter package) so it can use
|
||||
// the unexported newFormatter constructor without driving a real Postgres
|
||||
// connection. Covers the only listfilter cases whose emitted SQL differs
|
||||
// between SQLite and Postgres — the ones that go through JSONExtractString
|
||||
// (`name`, `description`). All other operators (=, !=, BETWEEN, LIKE, IN,
|
||||
// EXISTS, lower(...)) emit identical ANSI SQL on both dialects and are
|
||||
// covered by the SQLite tests in the listfilter package itself.
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/uptrace/bun/dialect/pgdialect"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes/listfilter"
|
||||
)
|
||||
|
||||
func TestListFilterCompile_Postgres(t *testing.T) {
|
||||
f := newFormatter(pgdialect.New())
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
query string
|
||||
wantSQL string
|
||||
wantArgs []any
|
||||
}{
|
||||
{
|
||||
name: "name = uses Postgres -> / ->> chain",
|
||||
query: `name = 'overview'`,
|
||||
wantSQL: `"dashboard"."data"->'data'->'display'->>'name' = ?`,
|
||||
wantArgs: []any{"overview"},
|
||||
},
|
||||
{
|
||||
name: "name CONTAINS — same JSON path, LIKE pattern",
|
||||
query: `name CONTAINS 'overview'`,
|
||||
wantSQL: `"dashboard"."data"->'data'->'display'->>'name' LIKE ?`,
|
||||
wantArgs: []any{"%overview%"},
|
||||
},
|
||||
{
|
||||
name: "name ILIKE — LOWER wraps the JSON path",
|
||||
query: `name ILIKE 'Prod%'`,
|
||||
wantSQL: `lower("dashboard"."data"->'data'->'display'->>'name') LIKE LOWER(?)`,
|
||||
wantArgs: []any{"Prod%"},
|
||||
},
|
||||
{
|
||||
name: "description = follows the same path shape",
|
||||
query: `description = 'd1'`,
|
||||
wantSQL: `"dashboard"."data"->'data'->'display'->>'description' = ?`,
|
||||
wantArgs: []any{"d1"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
out, err := listfilter.Compile(c.query, f)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out)
|
||||
assert.Equal(t, c.wantSQL, out.SQL)
|
||||
assert.Equal(t, c.wantArgs, out.Args)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,13 @@ export default defineConfig({
|
||||
signoz: {
|
||||
input: {
|
||||
target: '../docs/api/openapi.yml',
|
||||
// Perses' `common.JSONRef` (used by `DashboardGridItem.content`) has a
|
||||
// field tagged `json:"$ref"`, so our spec contains a property literally
|
||||
// named `$ref`.
|
||||
// Orval v8's validator (`@scalar/openapi-parser`) treats every `$ref` key
|
||||
// as a JSON Reference and aborts with `INVALID_REFERENCE` when the value isn't a URI string.
|
||||
// Safe to disable: yes, the spec is generated by `cmd/openapi.go` and gated by backend CI, not hand-edited.
|
||||
unsafeDisableValidation: true,
|
||||
},
|
||||
output: {
|
||||
target: './src/api/generated/services',
|
||||
@@ -27,7 +34,7 @@ export default defineConfig({
|
||||
signal: true,
|
||||
useOperationIdAsQueryKey: false,
|
||||
},
|
||||
useDates: true,
|
||||
useDates: false,
|
||||
useNamedParameters: true,
|
||||
enumGenerationType: 'enum',
|
||||
mutator: {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
const BANNED_COMPONENTS = {
|
||||
Typography: 'Use @signozhq/ui Typography instead of antd Typography.',
|
||||
Badge: 'Use @signozhq/ui/badge instead of antd Badge.',
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -47,7 +47,6 @@ export const TracesFunnels = Loadable(
|
||||
import(/* webpackChunkName: "Traces Funnels" */ 'pages/TracesModulePage'),
|
||||
);
|
||||
export const TracesFunnelDetails = Loadable(
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "Traces Funnel Details" */ 'pages/TracesModulePage'
|
||||
@@ -313,13 +312,6 @@ export const PublicDashboardPage = Loadable(
|
||||
),
|
||||
);
|
||||
|
||||
export const AlertTypeSelectionPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "Alert Type Selection Page" */ 'pages/AlertTypeSelection'
|
||||
),
|
||||
);
|
||||
|
||||
export const MeterExplorerPage = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "Meter Explorer Page" */ 'pages/MeterExplorer'),
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
AIAssistantPage,
|
||||
AlertHistory,
|
||||
AlertOverview,
|
||||
AlertTypeSelectionPage,
|
||||
AllAlertChannels,
|
||||
AllErrors,
|
||||
ApiMonitoring,
|
||||
@@ -144,18 +143,18 @@ const routes: AppRoutes[] = [
|
||||
// /trace-old serves V3 (URL-only access). Flip the two `component`
|
||||
// values back to release V3.
|
||||
{
|
||||
path: ROUTES.TRACE_DETAIL,
|
||||
path: ROUTES.TRACE_DETAIL_OLD,
|
||||
exact: true,
|
||||
component: TraceDetail,
|
||||
isPrivate: true,
|
||||
key: 'TRACE_DETAIL',
|
||||
key: 'TRACE_DETAIL_OLD',
|
||||
},
|
||||
{
|
||||
path: ROUTES.TRACE_DETAIL_OLD,
|
||||
path: ROUTES.TRACE_DETAIL,
|
||||
exact: true,
|
||||
component: TraceDetailV3,
|
||||
isPrivate: true,
|
||||
key: 'TRACE_DETAIL_OLD',
|
||||
key: 'TRACE_DETAIL',
|
||||
},
|
||||
{
|
||||
path: ROUTES.SETTINGS,
|
||||
@@ -213,13 +212,6 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'LIST_ALL_ALERT',
|
||||
},
|
||||
{
|
||||
path: ROUTES.ALERT_TYPE_SELECTION,
|
||||
exact: true,
|
||||
component: AlertTypeSelectionPage,
|
||||
isPrivate: true,
|
||||
key: 'ALERT_TYPE_SELECTION',
|
||||
},
|
||||
{
|
||||
path: ROUTES.ALERTS_NEW,
|
||||
exact: true,
|
||||
@@ -533,18 +525,6 @@ export const LIST_LICENSES: AppRoutes = {
|
||||
key: 'LIST_LICENSES',
|
||||
};
|
||||
|
||||
export const oldRoutes = [
|
||||
'/pipelines',
|
||||
'/logs-explorer',
|
||||
'/logs-explorer/live',
|
||||
'/logs-save-views',
|
||||
'/traces-save-views',
|
||||
'/settings/access-tokens',
|
||||
'/settings/api-keys',
|
||||
'/messaging-queues',
|
||||
'/alerts/edit',
|
||||
];
|
||||
|
||||
export const oldNewRoutesMapping: Record<string, string> = {
|
||||
'/pipelines': '/logs/pipelines',
|
||||
'/logs-explorer': '/logs/logs-explorer',
|
||||
@@ -555,7 +535,9 @@ export const oldNewRoutesMapping: Record<string, string> = {
|
||||
'/settings/api-keys': '/settings/service-accounts',
|
||||
'/messaging-queues': '/messaging-queues/overview',
|
||||
'/alerts/edit': '/alerts/overview',
|
||||
'/alerts/type-selection': '/alerts/new',
|
||||
};
|
||||
export const oldRoutes = Object.keys(oldNewRoutesMapping);
|
||||
|
||||
export const ROUTES_NOT_TO_BE_OVERRIDEN: string[] = [
|
||||
ROUTES.WORKSPACE_LOCKED,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
@@ -19,18 +18,32 @@ import type {
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
CreateDashboardV2201,
|
||||
CreatePublicDashboard201,
|
||||
CreatePublicDashboardPathParameters,
|
||||
DashboardtypesJSONPatchDocumentDTO,
|
||||
DashboardtypesPostableDashboardV2DTO,
|
||||
DashboardtypesPostablePublicDashboardDTO,
|
||||
DashboardtypesUpdatablePublicDashboardDTO,
|
||||
DeletePublicDashboardPathParameters,
|
||||
GetDashboardV2200,
|
||||
GetDashboardV2PathParameters,
|
||||
GetPublicDashboard200,
|
||||
GetPublicDashboardData200,
|
||||
GetPublicDashboardDataPathParameters,
|
||||
GetPublicDashboardPathParameters,
|
||||
GetPublicDashboardWidgetQueryRange200,
|
||||
GetPublicDashboardWidgetQueryRangePathParameters,
|
||||
ListDashboardsV2200,
|
||||
LockDashboardV2PathParameters,
|
||||
PatchDashboardV2200,
|
||||
PatchDashboardV2PathParameters,
|
||||
PinDashboardV2PathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
UnlockDashboardV2PathParameters,
|
||||
UnpinDashboardV2PathParameters,
|
||||
UpdateDashboardV2200,
|
||||
UpdateDashboardV2PathParameters,
|
||||
UpdatePublicDashboardPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
@@ -629,3 +642,786 @@ export const invalidateGetPublicDashboardWidgetQueryRange = async (
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a page of v2-shape dashboards for the calling user's org. Supports a filter DSL (`query`), sort (`updated_at`/`created_at`/`title`), order (`asc`/`desc`), and offset-based pagination (`limit`/`offset`). Pinned dashboards float to the top of each page.
|
||||
* @summary List dashboards (v2)
|
||||
*/
|
||||
export const listDashboardsV2 = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<ListDashboardsV2200>({
|
||||
url: `/api/v2/dashboards`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListDashboardsV2QueryKey = () => {
|
||||
return [`/api/v2/dashboards`] as const;
|
||||
};
|
||||
|
||||
export const getListDashboardsV2QueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listDashboardsV2>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listDashboardsV2>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListDashboardsV2QueryKey();
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof listDashboardsV2>>> = ({
|
||||
signal,
|
||||
}) => listDashboardsV2(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listDashboardsV2>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListDashboardsV2QueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listDashboardsV2>>
|
||||
>;
|
||||
export type ListDashboardsV2QueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List dashboards (v2)
|
||||
*/
|
||||
|
||||
export function useListDashboardsV2<
|
||||
TData = Awaited<ReturnType<typeof listDashboardsV2>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listDashboardsV2>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListDashboardsV2QueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List dashboards (v2)
|
||||
*/
|
||||
export const invalidateListDashboardsV2 = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListDashboardsV2QueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint creates a v2-shape dashboard with structured metadata, a typed data tree, and resolved tags.
|
||||
* @summary Create dashboard (v2)
|
||||
*/
|
||||
export const createDashboardV2 = (
|
||||
dashboardtypesPostableDashboardV2DTO?: BodyType<DashboardtypesPostableDashboardV2DTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateDashboardV2201>({
|
||||
url: `/api/v2/dashboards`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: dashboardtypesPostableDashboardV2DTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDashboardV2>>,
|
||||
TError,
|
||||
{ data?: BodyType<DashboardtypesPostableDashboardV2DTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDashboardV2>>,
|
||||
TError,
|
||||
{ data?: BodyType<DashboardtypesPostableDashboardV2DTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createDashboardV2'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof createDashboardV2>>,
|
||||
{ data?: BodyType<DashboardtypesPostableDashboardV2DTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createDashboardV2(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createDashboardV2>>
|
||||
>;
|
||||
export type CreateDashboardV2MutationBody =
|
||||
| BodyType<DashboardtypesPostableDashboardV2DTO>
|
||||
| undefined;
|
||||
export type CreateDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create dashboard (v2)
|
||||
*/
|
||||
export const useCreateDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDashboardV2>>,
|
||||
TError,
|
||||
{ data?: BodyType<DashboardtypesPostableDashboardV2DTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createDashboardV2>>,
|
||||
TError,
|
||||
{ data?: BodyType<DashboardtypesPostableDashboardV2DTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateDashboardV2MutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint returns a v2-shape dashboard with its tags and public sharing config (if any).
|
||||
* @summary Get dashboard (v2)
|
||||
*/
|
||||
export const getDashboardV2 = (
|
||||
{ id }: GetDashboardV2PathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetDashboardV2200>({
|
||||
url: `/api/v2/dashboards/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetDashboardV2QueryKey = ({
|
||||
id,
|
||||
}: GetDashboardV2PathParameters) => {
|
||||
return [`/api/v2/dashboards/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetDashboardV2QueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getDashboardV2>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetDashboardV2PathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getDashboardV2>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetDashboardV2QueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getDashboardV2>>> = ({
|
||||
signal,
|
||||
}) => getDashboardV2({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getDashboardV2>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetDashboardV2QueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getDashboardV2>>
|
||||
>;
|
||||
export type GetDashboardV2QueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get dashboard (v2)
|
||||
*/
|
||||
|
||||
export function useGetDashboardV2<
|
||||
TData = Awaited<ReturnType<typeof getDashboardV2>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetDashboardV2PathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getDashboardV2>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetDashboardV2QueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get dashboard (v2)
|
||||
*/
|
||||
export const invalidateGetDashboardV2 = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetDashboardV2PathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetDashboardV2QueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint applies an RFC 6902 JSON Patch to a v2-shape dashboard. The patch is applied against the postable view of the dashboard (metadata, data, tags), so individual panels, queries, variables, layouts, or tags can be updated without re-sending the rest of the dashboard. Locked dashboards are rejected.
|
||||
* @summary Patch dashboard (v2)
|
||||
*/
|
||||
export const patchDashboardV2 = (
|
||||
{ id }: PatchDashboardV2PathParameters,
|
||||
dashboardtypesJSONPatchDocumentDTONull?: BodyType<DashboardtypesJSONPatchDocumentDTO | null> | null,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<PatchDashboardV2200>({
|
||||
url: `/api/v2/dashboards/${id}`,
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: dashboardtypesJSONPatchDocumentDTONull,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPatchDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof patchDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchDashboardV2PathParameters;
|
||||
data?: BodyType<DashboardtypesJSONPatchDocumentDTO | null>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof patchDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchDashboardV2PathParameters;
|
||||
data?: BodyType<DashboardtypesJSONPatchDocumentDTO | null>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['patchDashboardV2'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof patchDashboardV2>>,
|
||||
{
|
||||
pathParams: PatchDashboardV2PathParameters;
|
||||
data?: BodyType<DashboardtypesJSONPatchDocumentDTO | null>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return patchDashboardV2(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type PatchDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof patchDashboardV2>>
|
||||
>;
|
||||
export type PatchDashboardV2MutationBody =
|
||||
| BodyType<DashboardtypesJSONPatchDocumentDTO | null>
|
||||
| undefined;
|
||||
export type PatchDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Patch dashboard (v2)
|
||||
*/
|
||||
export const usePatchDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof patchDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchDashboardV2PathParameters;
|
||||
data?: BodyType<DashboardtypesJSONPatchDocumentDTO | null>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof patchDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: PatchDashboardV2PathParameters;
|
||||
data?: BodyType<DashboardtypesJSONPatchDocumentDTO | null>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getPatchDashboardV2MutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint updates a v2-shape dashboard's metadata, data, and tag set. Locked dashboards are rejected.
|
||||
* @summary Update dashboard (v2)
|
||||
*/
|
||||
export const updateDashboardV2 = (
|
||||
{ id }: UpdateDashboardV2PathParameters,
|
||||
dashboardtypesPostableDashboardV2DTO?: BodyType<DashboardtypesPostableDashboardV2DTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<UpdateDashboardV2200>({
|
||||
url: `/api/v2/dashboards/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: dashboardtypesPostableDashboardV2DTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDashboardV2PathParameters;
|
||||
data?: BodyType<DashboardtypesPostableDashboardV2DTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDashboardV2PathParameters;
|
||||
data?: BodyType<DashboardtypesPostableDashboardV2DTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateDashboardV2'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof updateDashboardV2>>,
|
||||
{
|
||||
pathParams: UpdateDashboardV2PathParameters;
|
||||
data?: BodyType<DashboardtypesPostableDashboardV2DTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateDashboardV2(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateDashboardV2>>
|
||||
>;
|
||||
export type UpdateDashboardV2MutationBody =
|
||||
| BodyType<DashboardtypesPostableDashboardV2DTO>
|
||||
| undefined;
|
||||
export type UpdateDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update dashboard (v2)
|
||||
*/
|
||||
export const useUpdateDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDashboardV2PathParameters;
|
||||
data?: BodyType<DashboardtypesPostableDashboardV2DTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateDashboardV2>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDashboardV2PathParameters;
|
||||
data?: BodyType<DashboardtypesPostableDashboardV2DTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateDashboardV2MutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint unlocks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.
|
||||
* @summary Unlock dashboard (v2)
|
||||
*/
|
||||
export const unlockDashboardV2 = (
|
||||
{ id }: UnlockDashboardV2PathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v2/dashboards/${id}/lock`,
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUnlockDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof unlockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: UnlockDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof unlockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: UnlockDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['unlockDashboardV2'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof unlockDashboardV2>>,
|
||||
{ pathParams: UnlockDashboardV2PathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return unlockDashboardV2(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UnlockDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof unlockDashboardV2>>
|
||||
>;
|
||||
|
||||
export type UnlockDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Unlock dashboard (v2)
|
||||
*/
|
||||
export const useUnlockDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof unlockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: UnlockDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof unlockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: UnlockDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUnlockDashboardV2MutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint locks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.
|
||||
* @summary Lock dashboard (v2)
|
||||
*/
|
||||
export const lockDashboardV2 = (
|
||||
{ id }: LockDashboardV2PathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v2/dashboards/${id}/lock`,
|
||||
method: 'PUT',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getLockDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof lockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: LockDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof lockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: LockDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['lockDashboardV2'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof lockDashboardV2>>,
|
||||
{ pathParams: LockDashboardV2PathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return lockDashboardV2(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type LockDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof lockDashboardV2>>
|
||||
>;
|
||||
|
||||
export type LockDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Lock dashboard (v2)
|
||||
*/
|
||||
export const useLockDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof lockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: LockDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof lockDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: LockDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getLockDashboardV2MutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Removes the pin for the calling user. Idempotent — unpinning a dashboard that wasn't pinned still returns 204.
|
||||
* @summary Unpin a dashboard for the current user (v2)
|
||||
*/
|
||||
export const unpinDashboardV2 = (
|
||||
{ id }: UnpinDashboardV2PathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v2/dashboards/${id}/pins/me`,
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUnpinDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof unpinDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: UnpinDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof unpinDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: UnpinDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['unpinDashboardV2'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof unpinDashboardV2>>,
|
||||
{ pathParams: UnpinDashboardV2PathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return unpinDashboardV2(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UnpinDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof unpinDashboardV2>>
|
||||
>;
|
||||
|
||||
export type UnpinDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Unpin a dashboard for the current user (v2)
|
||||
*/
|
||||
export const useUnpinDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof unpinDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: UnpinDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof unpinDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: UnpinDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUnpinDashboardV2MutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Pins the dashboard for the calling user. A user can pin at most 10 dashboards; pinning when at the limit returns 409. Re-pinning an already-pinned dashboard is a no-op success.
|
||||
* @summary Pin a dashboard for the current user (v2)
|
||||
*/
|
||||
export const pinDashboardV2 = (
|
||||
{ id }: PinDashboardV2PathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v2/dashboards/${id}/pins/me`,
|
||||
method: 'PUT',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPinDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof pinDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: PinDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof pinDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: PinDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['pinDashboardV2'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof pinDashboardV2>>,
|
||||
{ pathParams: PinDashboardV2PathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return pinDashboardV2(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type PinDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof pinDashboardV2>>
|
||||
>;
|
||||
|
||||
export type PinDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Pin a dashboard for the current user (v2)
|
||||
*/
|
||||
export const usePinDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof pinDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: PinDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof pinDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: PinDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getPinDashboardV2MutationOptions(options));
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
@@ -19,6 +18,7 @@ import type {
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
AlertmanagertypesPostablePlannedMaintenanceDTO,
|
||||
CreateDowntimeSchedule201,
|
||||
DeleteDowntimeScheduleByIDPathParameters,
|
||||
GetDowntimeScheduleByID200,
|
||||
@@ -26,7 +26,6 @@ import type {
|
||||
ListDowntimeSchedules200,
|
||||
ListDowntimeSchedulesParams,
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesPostablePlannedMaintenanceDTO,
|
||||
UpdateDowntimeScheduleByIDPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
@@ -136,14 +135,14 @@ export const invalidateListDowntimeSchedules = async (
|
||||
* @summary Create downtime schedule
|
||||
*/
|
||||
export const createDowntimeSchedule = (
|
||||
ruletypesPostablePlannedMaintenanceDTO?: BodyType<RuletypesPostablePlannedMaintenanceDTO>,
|
||||
alertmanagertypesPostablePlannedMaintenanceDTO?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateDowntimeSchedule201>({
|
||||
url: `/api/v1/downtime_schedules`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: ruletypesPostablePlannedMaintenanceDTO,
|
||||
data: alertmanagertypesPostablePlannedMaintenanceDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
@@ -155,13 +154,13 @@ export const getCreateDowntimeScheduleMutationOptions = <
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
|
||||
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
|
||||
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createDowntimeSchedule'];
|
||||
@@ -175,7 +174,7 @@ export const getCreateDowntimeScheduleMutationOptions = <
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> }
|
||||
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
@@ -189,7 +188,7 @@ export type CreateDowntimeScheduleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>
|
||||
>;
|
||||
export type CreateDowntimeScheduleMutationBody =
|
||||
| BodyType<RuletypesPostablePlannedMaintenanceDTO>
|
||||
| BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>
|
||||
| undefined;
|
||||
export type CreateDowntimeScheduleMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
@@ -204,13 +203,13 @@ export const useCreateDowntimeSchedule = <
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
|
||||
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createDowntimeSchedule>>,
|
||||
TError,
|
||||
{ data?: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
|
||||
{ data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateDowntimeScheduleMutationOptions(options));
|
||||
@@ -404,14 +403,14 @@ export const invalidateGetDowntimeScheduleByID = async (
|
||||
*/
|
||||
export const updateDowntimeScheduleByID = (
|
||||
{ id }: UpdateDowntimeScheduleByIDPathParameters,
|
||||
ruletypesPostablePlannedMaintenanceDTO?: BodyType<RuletypesPostablePlannedMaintenanceDTO>,
|
||||
alertmanagertypesPostablePlannedMaintenanceDTO?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/downtime_schedules/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: ruletypesPostablePlannedMaintenanceDTO,
|
||||
data: alertmanagertypesPostablePlannedMaintenanceDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
@@ -425,7 +424,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -434,7 +433,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
@@ -451,7 +450,7 @@ export const getUpdateDowntimeScheduleByIDMutationOptions = <
|
||||
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
@@ -466,7 +465,7 @@ export type UpdateDowntimeScheduleByIDMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>
|
||||
>;
|
||||
export type UpdateDowntimeScheduleByIDMutationBody =
|
||||
| BodyType<RuletypesPostablePlannedMaintenanceDTO>
|
||||
| BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>
|
||||
| undefined;
|
||||
export type UpdateDowntimeScheduleByIDMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
@@ -483,7 +482,7 @@ export const useUpdateDowntimeScheduleByID = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -492,7 +491,7 @@ export const useUpdateDowntimeScheduleByID = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDowntimeScheduleByIDPathParameters;
|
||||
data?: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
|
||||
data?: BodyType<AlertmanagertypesPostablePlannedMaintenanceDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
* OpenAPI spec version: 0.0.1
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
.breadcrumb {
|
||||
padding-left: 16px;
|
||||
|
||||
ol {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:global(.ant-breadcrumb-separator) {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-color: var(--l1-border);
|
||||
margin: 16px 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
32
frontend/src/components/AlertBreadcrumb/AlertBreadcrumb.tsx
Normal file
32
frontend/src/components/AlertBreadcrumb/AlertBreadcrumb.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Breadcrumb, Divider } from 'antd';
|
||||
|
||||
import styles from './AlertBreadcrumb.module.scss';
|
||||
import BreadcrumbItem, { BreadcrumbItemConfig } from './BreadcrumbItem';
|
||||
|
||||
export interface AlertBreadcrumbProps {
|
||||
items: BreadcrumbItemConfig[];
|
||||
className?: string;
|
||||
showDivider?: boolean;
|
||||
}
|
||||
|
||||
function AlertBreadcrumb({
|
||||
items,
|
||||
className,
|
||||
showDivider = true,
|
||||
}: AlertBreadcrumbProps): JSX.Element {
|
||||
const breadcrumbItems = items.map((item) => ({
|
||||
title: <BreadcrumbItem {...item} />,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb
|
||||
className={`${styles.breadcrumb} ${className || ''}`}
|
||||
items={breadcrumbItems}
|
||||
/>
|
||||
{showDivider && <Divider className={styles.divider} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AlertBreadcrumb;
|
||||
@@ -0,0 +1,9 @@
|
||||
.item {
|
||||
--button-padding: 0;
|
||||
--button-font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
|
||||
.itemLast {
|
||||
color: var(--muted-foreground);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
45
frontend/src/components/AlertBreadcrumb/BreadcrumbItem.tsx
Normal file
45
frontend/src/components/AlertBreadcrumb/BreadcrumbItem.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import styles from './BreadcrumbItem.module.scss';
|
||||
|
||||
export type BreadcrumbItemConfig =
|
||||
| {
|
||||
title: string | null;
|
||||
route?: string;
|
||||
}
|
||||
| {
|
||||
title: string | null;
|
||||
isLast?: true;
|
||||
};
|
||||
|
||||
function BreadcrumbItem({
|
||||
title,
|
||||
...props
|
||||
}: BreadcrumbItemConfig): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
if ('isLast' in props) {
|
||||
return <div className={styles.itemLast}>{title}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className={styles.item}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
if (!('route' in props) || !props.route) {
|
||||
return;
|
||||
}
|
||||
|
||||
safeNavigate(props.route, { newTab: isModifierKeyPressed(e) });
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default BreadcrumbItem;
|
||||
6
frontend/src/components/AlertBreadcrumb/index.tsx
Normal file
6
frontend/src/components/AlertBreadcrumb/index.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default } from './AlertBreadcrumb';
|
||||
export {
|
||||
default as BreadcrumbItem,
|
||||
type BreadcrumbItemConfig,
|
||||
} from './BreadcrumbItem';
|
||||
export type { AlertBreadcrumbProps } from './AlertBreadcrumb';
|
||||
@@ -59,7 +59,7 @@ function getDeleteTooltip(
|
||||
|
||||
function getInviteButtonLabel(
|
||||
isLoading: boolean,
|
||||
existingToken: { expiresAt?: Date } | undefined,
|
||||
existingToken: { expiresAt?: string } | undefined,
|
||||
isExpired: boolean,
|
||||
notFound: boolean,
|
||||
): string {
|
||||
|
||||
@@ -18,21 +18,22 @@ import { Button } from '@signozhq/ui/button';
|
||||
import cx from 'classnames';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { GripVertical } from '@signozhq/icons';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import styles from './FieldsSettings.module.scss';
|
||||
import styles from './FieldsSelector.module.scss';
|
||||
|
||||
function SortableField({
|
||||
field,
|
||||
onRemove,
|
||||
allowDrag,
|
||||
}: {
|
||||
field: BaseAutocompleteData;
|
||||
onRemove: (field: BaseAutocompleteData) => void;
|
||||
field: TelemetryFieldKey;
|
||||
onRemove: (field: TelemetryFieldKey) => void;
|
||||
allowDrag: boolean;
|
||||
}): JSX.Element {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({ id: field.key });
|
||||
useSortable({ id: field.name });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
@@ -50,7 +51,7 @@ function SortableField({
|
||||
>
|
||||
<div {...attributes} {...listeners} className={styles.dragHandle}>
|
||||
{allowDrag && <GripVertical size={14} />}
|
||||
<span className={styles.fieldKey}>{field.key}</span>
|
||||
<span className={styles.fieldKey}>{field.name}</span>
|
||||
</div>
|
||||
<Button
|
||||
className={cx(styles.removeBtn, 'periscope-btn')}
|
||||
@@ -67,41 +68,52 @@ function SortableField({
|
||||
|
||||
interface AddedFieldsProps {
|
||||
inputValue: string;
|
||||
fields: BaseAutocompleteData[];
|
||||
onFieldsChange: (fields: BaseAutocompleteData[]) => void;
|
||||
fields: TelemetryFieldKey[];
|
||||
onFieldsChange: (fields: TelemetryFieldKey[]) => void;
|
||||
maxFields?: number;
|
||||
}
|
||||
|
||||
function AddedFields({
|
||||
inputValue,
|
||||
fields,
|
||||
onFieldsChange,
|
||||
maxFields,
|
||||
}: AddedFieldsProps): JSX.Element {
|
||||
const sensors = useSensors(useSensor(PointerSensor));
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent): void => {
|
||||
const { active, over } = event;
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = fields.findIndex((f) => f.key === active.id);
|
||||
const newIndex = fields.findIndex((f) => f.key === over.id);
|
||||
const oldIndex = fields.findIndex((f) => f.name === active.id);
|
||||
const newIndex = fields.findIndex((f) => f.name === over.id);
|
||||
onFieldsChange(arrayMove(fields, oldIndex, newIndex));
|
||||
}
|
||||
};
|
||||
|
||||
const filteredFields = useMemo(
|
||||
() =>
|
||||
fields.filter((f) => f.key.toLowerCase().includes(inputValue.toLowerCase())),
|
||||
fields.filter((f) =>
|
||||
f.name.toLowerCase().includes(inputValue.toLowerCase()),
|
||||
),
|
||||
[fields, inputValue],
|
||||
);
|
||||
|
||||
const handleRemove = (field: BaseAutocompleteData): void => {
|
||||
onFieldsChange(fields.filter((f) => f.key !== field.key));
|
||||
const handleRemove = (field: TelemetryFieldKey): void => {
|
||||
onFieldsChange(fields.filter((f) => f.name !== field.name));
|
||||
};
|
||||
|
||||
const allowDrag = inputValue.length === 0;
|
||||
|
||||
return (
|
||||
<div className={cx(styles.section, styles.sectionAdded)}>
|
||||
<div className={styles.sectionHeader}>ADDED FIELDS</div>
|
||||
<div className={styles.sectionHeader}>
|
||||
<span>ADDED FIELDS</span>
|
||||
{maxFields !== undefined && (
|
||||
<Typography.Text size="sm" weight="medium" color="muted">
|
||||
Max Allowed: {maxFields}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.addedList}>
|
||||
<OverlayScrollbar>
|
||||
<DndContext
|
||||
@@ -113,13 +125,13 @@ function AddedFields({
|
||||
<div className={styles.noValues}>No values found</div>
|
||||
) : (
|
||||
<SortableContext
|
||||
items={fields.map((f) => f.key)}
|
||||
items={fields.map((f) => f.name)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
disabled={!allowDrag}
|
||||
>
|
||||
{filteredFields.map((field) => (
|
||||
<SortableField
|
||||
key={field.key}
|
||||
key={field.name}
|
||||
field={field}
|
||||
onRemove={handleRemove}
|
||||
allowDrag={allowDrag}
|
||||
@@ -56,12 +56,14 @@
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
@@ -89,13 +91,6 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.limitHint {
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.fieldItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
176
frontend/src/components/FieldsSelector/FieldsSelector.tsx
Normal file
176
frontend/src/components/FieldsSelector/FieldsSelector.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
|
||||
import { FloatingPanel } from 'periscope/components/FloatingPanel';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import AddedFields from './AddedFields';
|
||||
import OtherFields from './OtherFields';
|
||||
|
||||
import styles from './FieldsSelector.module.scss';
|
||||
|
||||
const DEFAULT_PANEL_WIDTH = 350;
|
||||
const DEFAULT_PANEL_HEIGHT_OFFSET = 100;
|
||||
const DEFAULT_PANEL_RIGHT_INSET = 100;
|
||||
const DEFAULT_PANEL_TOP_INSET = 50;
|
||||
|
||||
interface FieldsSelectorProps {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
fields: TelemetryFieldKey[];
|
||||
onFieldsChange: (fields: TelemetryFieldKey[]) => void;
|
||||
onClose: () => void;
|
||||
signal: DataSource;
|
||||
maxFields?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
defaultPosition?: { x: number; y: number };
|
||||
}
|
||||
|
||||
function FieldsSelector({
|
||||
isOpen,
|
||||
title,
|
||||
fields,
|
||||
onFieldsChange,
|
||||
onClose,
|
||||
signal,
|
||||
maxFields,
|
||||
width = DEFAULT_PANEL_WIDTH,
|
||||
height,
|
||||
defaultPosition,
|
||||
}: FieldsSelectorProps): JSX.Element | null {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolvedHeight =
|
||||
height ?? window.innerHeight - DEFAULT_PANEL_HEIGHT_OFFSET;
|
||||
const resolvedPosition = defaultPosition ?? {
|
||||
x: window.innerWidth - width - DEFAULT_PANEL_RIGHT_INSET,
|
||||
y: DEFAULT_PANEL_TOP_INSET,
|
||||
};
|
||||
const [draftFields, setDraftFields] = useState<TelemetryFieldKey[]>(fields);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [debouncedInputValue, setDebouncedInputValue] = useState('');
|
||||
|
||||
const debouncedUpdate = useDebouncedFn((value) => {
|
||||
setDebouncedInputValue(value as string);
|
||||
}, 400);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const value = e.target.value.trim().toLowerCase();
|
||||
setInputValue(value);
|
||||
debouncedUpdate(value);
|
||||
},
|
||||
[debouncedUpdate],
|
||||
);
|
||||
|
||||
const handleAdd = useCallback(
|
||||
(field: TelemetryFieldKey): void => {
|
||||
if (maxFields !== undefined && draftFields.length >= maxFields) {
|
||||
return;
|
||||
}
|
||||
if (draftFields.some((f) => f.name === field.name)) {
|
||||
return;
|
||||
}
|
||||
setDraftFields((prev) => [...prev, field]);
|
||||
},
|
||||
[draftFields, maxFields],
|
||||
);
|
||||
|
||||
const handleSave = useCallback((): void => {
|
||||
onFieldsChange(draftFields);
|
||||
toast.success('Saved successfully', {
|
||||
position: 'top-right',
|
||||
});
|
||||
onClose();
|
||||
}, [draftFields, onFieldsChange, onClose]);
|
||||
|
||||
const handleDiscard = useCallback((): void => {
|
||||
setDraftFields(fields);
|
||||
}, [fields]);
|
||||
|
||||
const hasUnsavedChanges = useMemo(
|
||||
() =>
|
||||
!(
|
||||
draftFields.length === fields.length &&
|
||||
draftFields.every((f, i) => f.name === fields[i]?.name)
|
||||
),
|
||||
[draftFields, fields],
|
||||
);
|
||||
|
||||
const isAtLimit = maxFields !== undefined && draftFields.length >= maxFields;
|
||||
|
||||
return (
|
||||
<FloatingPanel
|
||||
isOpen
|
||||
width={width}
|
||||
height={resolvedHeight}
|
||||
defaultPosition={resolvedPosition}
|
||||
enableResizing={false}
|
||||
>
|
||||
<div className={styles.root}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>
|
||||
<TableColumnsSplit size={16} />
|
||||
{title}
|
||||
</div>
|
||||
<X className={styles.closeIcon} size={16} onClick={onClose} />
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<Input
|
||||
className={styles.searchInput}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
placeholder="Search for a field..."
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<AddedFields
|
||||
inputValue={inputValue}
|
||||
fields={draftFields}
|
||||
onFieldsChange={setDraftFields}
|
||||
maxFields={maxFields}
|
||||
/>
|
||||
|
||||
<OtherFields
|
||||
signal={signal}
|
||||
debouncedInputValue={debouncedInputValue}
|
||||
addedFields={draftFields}
|
||||
onAdd={handleAdd}
|
||||
isAtLimit={isAtLimit}
|
||||
/>
|
||||
|
||||
{hasUnsavedChanges && (
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={handleDiscard}
|
||||
prefix={<X width={14} height={14} />}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={handleSave}
|
||||
prefix={<Check width={14} height={14} />}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</FloatingPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export default FieldsSelector;
|
||||
@@ -4,51 +4,58 @@ import { Skeleton } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
||||
import {
|
||||
FieldContext,
|
||||
FieldDataType,
|
||||
SignalType,
|
||||
TelemetryFieldKey,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import styles from './FieldsSettings.module.scss';
|
||||
import styles from './FieldsSelector.module.scss';
|
||||
|
||||
interface OtherFieldsProps {
|
||||
dataSource: DataSource;
|
||||
signal: DataSource;
|
||||
debouncedInputValue: string;
|
||||
addedFields: BaseAutocompleteData[];
|
||||
onAdd: (field: BaseAutocompleteData) => void;
|
||||
addedFields: TelemetryFieldKey[];
|
||||
onAdd: (field: TelemetryFieldKey) => void;
|
||||
isAtLimit: boolean;
|
||||
}
|
||||
|
||||
function OtherFields({
|
||||
dataSource,
|
||||
signal,
|
||||
debouncedInputValue,
|
||||
addedFields,
|
||||
onAdd,
|
||||
isAtLimit,
|
||||
}: OtherFieldsProps): JSX.Element {
|
||||
// API call to get available attribute keys
|
||||
const { data, isFetching } = useGetAggregateKeys(
|
||||
const { data, isFetching } = useGetQueryKeySuggestions(
|
||||
{
|
||||
signal,
|
||||
searchText: debouncedInputValue,
|
||||
dataSource,
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_OTHER_FILTERS,
|
||||
'preview-fields',
|
||||
REACT_QUERY_KEY.GET_FIELDS_SELECTOR_SUGGESTIONS,
|
||||
signal,
|
||||
debouncedInputValue,
|
||||
],
|
||||
enabled: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Filter out already-added fields, match on .key from API response objects
|
||||
const otherFields = useMemo(() => {
|
||||
const attributes = data?.payload?.attributeKeys || [];
|
||||
const addedKeys = new Set(addedFields.map((f) => f.key));
|
||||
return attributes.filter((attr) => !addedKeys.has(attr.key));
|
||||
const otherFields: TelemetryFieldKey[] = useMemo(() => {
|
||||
const suggestions = Object.values(data?.data.data.keys || {}).flat();
|
||||
const addedNames = new Set(addedFields.map((f) => f.name));
|
||||
return suggestions
|
||||
.filter((attr) => !addedNames.has(attr.name))
|
||||
.map((attr) => ({
|
||||
...attr,
|
||||
signal: attr.signal as SignalType,
|
||||
fieldContext: attr.fieldContext as FieldContext,
|
||||
fieldDataType: attr.fieldDataType as FieldDataType,
|
||||
}));
|
||||
}, [data, addedFields]);
|
||||
|
||||
if (isFetching) {
|
||||
@@ -76,10 +83,10 @@ function OtherFields({
|
||||
) : (
|
||||
otherFields.map((attr) => (
|
||||
<div
|
||||
key={attr.key}
|
||||
key={attr.name}
|
||||
className={cx(styles.fieldItem, styles.otherFieldItem)}
|
||||
>
|
||||
<span className={styles.fieldKey}>{attr.key}</span>
|
||||
<span className={styles.fieldKey}>{attr.name}</span>
|
||||
{!isAtLimit && (
|
||||
<Button
|
||||
className={cx(styles.addBtn, 'periscope-btn')}
|
||||
@@ -94,7 +101,6 @@ function OtherFields({
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{isAtLimit && <div className={styles.limitHint}>Maximum 10 fields</div>}
|
||||
</>
|
||||
</OverlayScrollbar>
|
||||
</div>
|
||||
1
frontend/src/components/FieldsSelector/index.ts
Normal file
1
frontend/src/components/FieldsSelector/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FieldsSelector';
|
||||
@@ -5,6 +5,8 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Popover } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { AIAssistantEvents } from 'container/AIAssistant/events';
|
||||
import { normalizePage } from 'container/AIAssistant/hooks/useAIAssistantAnalyticsContext';
|
||||
import {
|
||||
openAIAssistant,
|
||||
useAIAssistantStore,
|
||||
@@ -50,6 +52,14 @@ function HeaderRightSection({
|
||||
setOpenAnnouncementsModal(false);
|
||||
}, [location.pathname]);
|
||||
|
||||
const handleOpenAIAssistant = useCallback((): void => {
|
||||
void logEvent(AIAssistantEvents.Opened, {
|
||||
source: 'header',
|
||||
currentPage: normalizePage(location.pathname),
|
||||
});
|
||||
openAIAssistant();
|
||||
}, [location.pathname]);
|
||||
|
||||
const handleOpenShareURLModal = useCallback((): void => {
|
||||
logEvent('Share: Clicked', {
|
||||
page: location.pathname,
|
||||
@@ -101,7 +111,7 @@ function HeaderRightSection({
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
onClick={openAIAssistant}
|
||||
onClick={handleOpenAIAssistant}
|
||||
aria-label={
|
||||
showHeaderPendingBadge
|
||||
? pendingUserInputCount === 1
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
import { JsonView } from 'periscope/components/JsonView';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILogBody } from 'types/api/logs/log';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
@@ -217,20 +218,17 @@ function LogDetailInner({
|
||||
|
||||
const logBody = useMemo(() => {
|
||||
if (!isBodyJsonQueryEnabled) {
|
||||
return log?.body || '';
|
||||
return (log?.body as string) ?? '';
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.parse(log?.body || '');
|
||||
|
||||
if (typeof json?.message === 'string' && json.message !== '') {
|
||||
return json.message;
|
||||
}
|
||||
|
||||
return log?.body || '';
|
||||
} catch {
|
||||
return log?.body || '';
|
||||
// Feature enabled: body is always a map; message is always a string
|
||||
const bodyObj = log?.body as ILogBody;
|
||||
if (!bodyObj) {
|
||||
return '';
|
||||
}
|
||||
if (bodyObj.message) {
|
||||
return bodyObj.message;
|
||||
}
|
||||
return JSON.stringify(bodyObj);
|
||||
}, [isBodyJsonQueryEnabled, log?.body]);
|
||||
|
||||
const htmlBody = useMemo(
|
||||
|
||||
@@ -9,7 +9,10 @@ import { Color } from '@signozhq/design-tokens';
|
||||
import { Tooltip } from 'antd';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
|
||||
import {
|
||||
getBodyDisplayString,
|
||||
getSanitizedLogBody,
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
// hooks
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -99,7 +102,7 @@ function RawLogView({
|
||||
// Check if body is selected
|
||||
const showBody = selectedFields.some((field) => field.name === 'body');
|
||||
if (showBody) {
|
||||
parts.push(`${attributesText} ${data.body}`);
|
||||
parts.push(`${attributesText} ${getBodyDisplayString(data.body)}`);
|
||||
} else {
|
||||
parts.push(attributesText);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ import type { ReactElement } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
|
||||
import {
|
||||
getBodyDisplayString,
|
||||
getSanitizedLogBody,
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
@@ -87,7 +90,7 @@ export function useLogsTableColumns({
|
||||
? {
|
||||
id: 'body',
|
||||
header: 'Body',
|
||||
accessorFn: (log): string => log.body,
|
||||
accessorFn: (log): string => getBodyDisplayString(log.body),
|
||||
canBeHidden: false,
|
||||
width: { default: '100%', min: 300 },
|
||||
cell: ({ value, isActive }): ReactElement => (
|
||||
|
||||
@@ -144,6 +144,7 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
|
||||
loading={loading}
|
||||
notFoundContent={notFoundContent}
|
||||
options={options}
|
||||
optionFilterProp="label"
|
||||
optionRender={(option): JSX.Element => (
|
||||
<Checkbox
|
||||
checked={value.includes(option.value as string)}
|
||||
@@ -162,6 +163,7 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
|
||||
return (
|
||||
<Select
|
||||
id={id}
|
||||
showSearch
|
||||
value={value || undefined}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
@@ -170,6 +172,7 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
|
||||
loading={loading}
|
||||
notFoundContent={notFoundContent}
|
||||
options={options}
|
||||
optionFilterProp="label"
|
||||
getPopupContainer={getPopupContainer}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
@@ -28,7 +28,7 @@ const mockKey: ServiceaccounttypesGettableFactorAPIKeyDTO = {
|
||||
id: 'key-1',
|
||||
name: 'Original Key Name',
|
||||
expiresAt: 0,
|
||||
lastObservedAt: null as unknown as Date,
|
||||
lastObservedAt: null as unknown as string,
|
||||
serviceAccountId: 'sa-1',
|
||||
};
|
||||
|
||||
|
||||
@@ -29,14 +29,14 @@ const keys: ServiceaccounttypesGettableFactorAPIKeyDTO[] = [
|
||||
id: 'key-1',
|
||||
name: 'Production Key',
|
||||
expiresAt: 0,
|
||||
lastObservedAt: null as unknown as Date,
|
||||
lastObservedAt: null as unknown as string,
|
||||
serviceAccountId: 'sa-1',
|
||||
},
|
||||
{
|
||||
id: 'key-2',
|
||||
name: 'Staging Key',
|
||||
expiresAt: 1924905600, // 2030-12-31
|
||||
lastObservedAt: new Date('2026-03-10T10:00:00Z'),
|
||||
lastObservedAt: '2026-03-10T10:00:00Z',
|
||||
serviceAccountId: 'sa-1',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -626,6 +626,10 @@ function TanStackTableInner<TData>(
|
||||
onChange={(value): void => {
|
||||
setLimit(+value);
|
||||
pagination.onLimitChange?.(+value);
|
||||
if (page !== 1) {
|
||||
setPage(1);
|
||||
pagination.onPageChange?.(1);
|
||||
}
|
||||
}}
|
||||
items={paginationPageSizeItems}
|
||||
/>
|
||||
|
||||
@@ -401,6 +401,62 @@ describe('TanStackTableView Integration', () => {
|
||||
expect(onLimitChange).toHaveBeenCalledWith(20);
|
||||
});
|
||||
});
|
||||
|
||||
it('resets page to 1 when limit changes', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
|
||||
const onPageChange = jest.fn();
|
||||
|
||||
renderTanStackTable({
|
||||
props: {
|
||||
pagination: { total: 100, defaultPage: 1, defaultLimit: 10, onPageChange },
|
||||
enableQueryParams: true,
|
||||
},
|
||||
onUrlUpdate,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Navigate to page 2
|
||||
const nav = screen.getByRole('navigation');
|
||||
const page2 = Array.from(nav.querySelectorAll('button')).find(
|
||||
(btn) => btn.textContent?.trim() === '2',
|
||||
);
|
||||
if (!page2) {
|
||||
throw new Error('Page 2 button not found in pagination');
|
||||
}
|
||||
await user.click(page2);
|
||||
|
||||
await waitFor(() => {
|
||||
const lastPage = onUrlUpdate.mock.calls
|
||||
.map((call) => call[0].searchParams.get('page'))
|
||||
.filter(Boolean)
|
||||
.pop();
|
||||
expect(lastPage).toBe('2');
|
||||
});
|
||||
|
||||
// Change page size
|
||||
const comboboxTrigger = document.querySelector(
|
||||
'button[aria-haspopup="dialog"]',
|
||||
) as HTMLElement;
|
||||
await user.click(comboboxTrigger);
|
||||
|
||||
const option20 = await screen.findByRole('option', { name: '20' });
|
||||
await user.click(option20);
|
||||
|
||||
// Verify page reset to 1 (nuqs removes default values from URL)
|
||||
await waitFor(() => {
|
||||
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
|
||||
const lastPage = lastCall[0].searchParams.get('page');
|
||||
expect(lastPage === '1' || lastPage === null).toBe(true);
|
||||
expect(lastCall[0].searchParams.get('limit')).toBe('20');
|
||||
});
|
||||
|
||||
// Verify onPageChange callback was called with 1
|
||||
expect(onPageChange).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sorting', () => {
|
||||
|
||||
@@ -10,4 +10,6 @@ export enum FeatureKeys {
|
||||
ONBOARDING_V3 = 'onboarding_v3',
|
||||
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
|
||||
USE_JSON_BODY = 'use_json_body',
|
||||
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
|
||||
DASHBOARD_V2 = 'dashboard_v2',
|
||||
}
|
||||
|
||||
@@ -108,4 +108,7 @@ export const REACT_QUERY_KEY = {
|
||||
|
||||
// Dashboard Grid Card Query Keys
|
||||
DASHBOARD_GRID_CARD_QUERY_RANGE: 'DASHBOARD_GRID_CARD_QUERY_RANGE',
|
||||
|
||||
// Fields Selector Query Keys
|
||||
GET_FIELDS_SELECTOR_SUGGESTIONS: 'GET_FIELDS_SELECTOR_SUGGESTIONS',
|
||||
} as const;
|
||||
|
||||
@@ -29,7 +29,6 @@ const ROUTES = {
|
||||
ALERTS_NEW: '/alerts/new',
|
||||
ALERT_HISTORY: '/alerts/history',
|
||||
ALERT_OVERVIEW: '/alerts/overview',
|
||||
ALERT_TYPE_SELECTION: '/alerts/type-selection',
|
||||
ALL_CHANNELS: '/settings/channels',
|
||||
CHANNELS_NEW: '/settings/channels/new',
|
||||
CHANNELS_EDIT: '/settings/channels/edit/:channelId',
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { History, Maximize2, Minus, Plus, Sparkles, X } from '@signozhq/icons';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import HistorySidebar from '../components/ConversationsList';
|
||||
import ConversationView from '../ConversationView';
|
||||
import { AIAssistantEvents } from '../events';
|
||||
import {
|
||||
normalizePage,
|
||||
useAIAssistantAnalyticsContext,
|
||||
} from '../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../store/useAIAssistantStore';
|
||||
import { VariantContext } from '../VariantContext';
|
||||
|
||||
@@ -24,6 +31,7 @@ import styles from './AIAssistantModal.module.scss';
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function AIAssistantModal(): JSX.Element | null {
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
|
||||
const isOpen = useAIAssistantStore((s) => s.isModalOpen);
|
||||
@@ -36,6 +44,7 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
const startNewConversation = useAIAssistantStore(
|
||||
(s) => s.startNewConversation,
|
||||
);
|
||||
const analyticsCtx = useAIAssistantAnalyticsContext();
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent): void => {
|
||||
@@ -55,6 +64,10 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
} else {
|
||||
startNewConversation();
|
||||
setShowHistory(false);
|
||||
void logEvent(AIAssistantEvents.Opened, {
|
||||
source: 'shortcut',
|
||||
currentPage: normalizePage(pathname),
|
||||
});
|
||||
openModal();
|
||||
}
|
||||
return;
|
||||
@@ -68,7 +81,7 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return (): void => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, openModal, closeModal, startNewConversation]);
|
||||
}, [isOpen, openModal, closeModal, startNewConversation, pathname]);
|
||||
|
||||
// ── Handlers ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -77,15 +90,28 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
return;
|
||||
}
|
||||
closeModal();
|
||||
// Router state tells AIAssistantPage to skip its mount-time Opened fire:
|
||||
// the assistant was already open in the modal, so this is a surface
|
||||
// switch, not a new open.
|
||||
history.push(
|
||||
ROUTES.AI_ASSISTANT.replace(':conversationId', activeConversationId),
|
||||
{ fromInApp: true },
|
||||
);
|
||||
}, [activeConversationId, closeModal, history]);
|
||||
|
||||
const handleNew = useCallback(() => {
|
||||
void logEvent(AIAssistantEvents.NewChatClicked, {
|
||||
...analyticsCtx,
|
||||
// useAIAssistantAnalyticsContext() runs above this component's
|
||||
// VariantContext.Provider, so the hook reports the default 'page'
|
||||
// mode. Override here: the modal collapses to 'sidepane' in our
|
||||
// taxonomy alongside the drawer.
|
||||
mode: 'sidepane',
|
||||
source: 'header',
|
||||
});
|
||||
startNewConversation();
|
||||
setShowHistory(false);
|
||||
}, [startNewConversation]);
|
||||
}, [startNewConversation, analyticsCtx]);
|
||||
|
||||
const handleHistorySelect = useCallback(() => {
|
||||
setShowHistory(false);
|
||||
|
||||
@@ -5,8 +5,12 @@ import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { History, Maximize2, Plus, Sparkles, X } from '@signozhq/icons';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import ConversationsList from '../components/ConversationsList';
|
||||
import ConversationView from '../ConversationView';
|
||||
import { AIAssistantEvents } from '../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../store/useAIAssistantStore';
|
||||
import { VariantContext } from '../VariantContext';
|
||||
|
||||
@@ -32,21 +36,35 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
const startNewConversation = useAIAssistantStore(
|
||||
(s) => s.startNewConversation,
|
||||
);
|
||||
const analyticsCtx = useAIAssistantAnalyticsContext();
|
||||
|
||||
const handleExpand = useCallback(() => {
|
||||
if (!activeConversationId) {
|
||||
return;
|
||||
}
|
||||
closeDrawer();
|
||||
// Router state tells AIAssistantPage to skip its mount-time Opened fire:
|
||||
// the assistant was already open in the drawer, so this is a surface
|
||||
// switch, not a new open.
|
||||
history.push(
|
||||
ROUTES.AI_ASSISTANT.replace(':conversationId', activeConversationId),
|
||||
{ fromInApp: true },
|
||||
);
|
||||
}, [activeConversationId, closeDrawer, history]);
|
||||
|
||||
const handleNew = useCallback(() => {
|
||||
void logEvent(AIAssistantEvents.NewChatClicked, {
|
||||
...analyticsCtx,
|
||||
// useAIAssistantAnalyticsContext() runs above this component's
|
||||
// VariantContext.Provider, so the hook reports the default 'page'
|
||||
// mode. Override here: this handler only runs when the drawer
|
||||
// itself is mounted, which is unambiguously the sidepane surface.
|
||||
mode: 'sidepane',
|
||||
source: 'header',
|
||||
});
|
||||
startNewConversation();
|
||||
setShowHistory(false);
|
||||
}, [startNewConversation]);
|
||||
}, [startNewConversation, analyticsCtx]);
|
||||
|
||||
// When user picks a conversation from the list, close the sidebar
|
||||
const handleHistorySelect = useCallback(() => {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { useCallback } from 'react';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { Bot } from '@signozhq/icons';
|
||||
|
||||
import { AIAssistantEvents } from '../events';
|
||||
import { normalizePage } from '../hooks/useAIAssistantAnalyticsContext';
|
||||
import {
|
||||
openAIAssistant,
|
||||
useAIAssistantStore,
|
||||
@@ -25,6 +29,14 @@ export default function AIAssistantTrigger(): JSX.Element | null {
|
||||
exact: true,
|
||||
});
|
||||
|
||||
const handleOpen = useCallback((): void => {
|
||||
void logEvent(AIAssistantEvents.Opened, {
|
||||
source: 'icon',
|
||||
currentPage: normalizePage(pathname),
|
||||
});
|
||||
openAIAssistant();
|
||||
}, [pathname]);
|
||||
|
||||
if (isDrawerOpen || isModalOpen || isFullScreenPage) {
|
||||
return null;
|
||||
}
|
||||
@@ -35,7 +47,7 @@ export default function AIAssistantTrigger(): JSX.Element | null {
|
||||
variant="solid"
|
||||
color="primary"
|
||||
className={styles.trigger}
|
||||
onClick={openAIAssistant}
|
||||
onClick={handleOpen}
|
||||
aria-label="Open AI Assistant"
|
||||
>
|
||||
<Bot size={20} />
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import cx from 'classnames';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import ChatInput, { autoContextKey } from '../components/ChatInput';
|
||||
import ConversationSkeleton from '../components/ConversationSkeleton';
|
||||
import VirtualizedMessages from '../components/VirtualizedMessages';
|
||||
import { AIAssistantEvents } from '../events';
|
||||
import { getAutoContexts } from '../getAutoContexts';
|
||||
import { useAIAssistantAnalyticsContext } from '../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../store/useAIAssistantStore';
|
||||
import { MessageAttachment } from '../types';
|
||||
import { MessageContext } from '../../../api/ai-assistant/chat';
|
||||
@@ -39,6 +43,7 @@ export default function ConversationView({
|
||||
);
|
||||
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
|
||||
const cancelStream = useAIAssistantStore((s) => s.cancelStream);
|
||||
const analyticsCtx = useAIAssistantAnalyticsContext(conversationId);
|
||||
|
||||
// Auto-derived contexts come from the route the user is currently looking
|
||||
// at (dashboard detail, service metrics, an explorer, …). Skip when the
|
||||
@@ -82,14 +87,50 @@ export default function ConversationView({
|
||||
attachments?: MessageAttachment[],
|
||||
contexts?: MessageContext[],
|
||||
) => {
|
||||
const hasAuto = contexts?.some((c) => c.source === 'auto') ?? false;
|
||||
const hasManual = contexts?.some((c) => c.source === 'mention') ?? false;
|
||||
let contextType: 'manual' | 'auto' | 'both' | undefined;
|
||||
if (hasAuto && hasManual) {
|
||||
contextType = 'both';
|
||||
} else if (hasAuto) {
|
||||
contextType = 'auto';
|
||||
} else if (hasManual) {
|
||||
contextType = 'manual';
|
||||
}
|
||||
void logEvent(AIAssistantEvents.MessageSent, {
|
||||
...analyticsCtx,
|
||||
queryLength: text.length,
|
||||
hasContext: hasAuto || hasManual,
|
||||
contextType,
|
||||
respondingToClarification: Boolean(pendingClarificationHere),
|
||||
});
|
||||
void sendMessage(text, attachments, contexts);
|
||||
},
|
||||
[sendMessage],
|
||||
[sendMessage, analyticsCtx, pendingClarificationHere],
|
||||
);
|
||||
|
||||
// Wall-clock timestamp of the current streaming start, used to compute
|
||||
// `secondsSinceStart` on Cancel clicked. Cleared whenever streaming ends.
|
||||
const streamStartedAtRef = useRef<number | null>(null);
|
||||
useEffect(() => {
|
||||
if (!isStreamingHere) {
|
||||
streamStartedAtRef.current = null;
|
||||
return;
|
||||
}
|
||||
if (streamStartedAtRef.current === null) {
|
||||
streamStartedAtRef.current = Date.now();
|
||||
}
|
||||
}, [isStreamingHere]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
const startedAt = streamStartedAtRef.current;
|
||||
void logEvent(AIAssistantEvents.CancelClicked, {
|
||||
threadId: analyticsCtx.threadId,
|
||||
secondsSinceStart:
|
||||
startedAt !== null ? Math.round((Date.now() - startedAt) / 1000) : null,
|
||||
});
|
||||
cancelStream(conversationId);
|
||||
}, [cancelStream, conversationId]);
|
||||
}, [cancelStream, conversationId, analyticsCtx.threadId]);
|
||||
|
||||
const messages = conversation?.messages ?? [];
|
||||
const showDisclaimer = messages.length > 0;
|
||||
@@ -134,6 +175,7 @@ export default function ConversationView({
|
||||
conversationId={conversationId}
|
||||
messages={messages}
|
||||
isStreaming={isStreamingHere}
|
||||
onSendSuggestedPrompt={(text): void => handleSend(text)}
|
||||
/>
|
||||
{showDisclaimer && (
|
||||
<div className={disclaimerClass} role="note" aria-live="polite">
|
||||
|
||||
@@ -41,12 +41,68 @@ import {
|
||||
Undo,
|
||||
} from '@signozhq/icons';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import { AIAssistantEvents, SuggestedPromptCategory } from '../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
|
||||
|
||||
import styles from './ActionsSection.module.scss';
|
||||
|
||||
interface ActionsSectionProps {
|
||||
actions: MessageActionDTO[];
|
||||
/** ID of the assistant message these actions belong to — used in analytics. */
|
||||
messageId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resource-type strings the backend uses for `open_resource` and rollback
|
||||
* actions. Centralized here so the route/module lookups below stay in sync.
|
||||
*/
|
||||
const ResourceType = {
|
||||
dashboard: 'dashboard',
|
||||
alert: 'alert',
|
||||
service: 'service',
|
||||
saved_view: 'saved_view',
|
||||
logs_explorer: 'logs_explorer',
|
||||
traces_explorer: 'traces_explorer',
|
||||
metrics_explorer: 'metrics_explorer',
|
||||
} as const;
|
||||
|
||||
/** Maps an open_resource action's resourceType to its product module name. */
|
||||
function targetModuleForResource(resourceType: string): string | null {
|
||||
switch (resourceType) {
|
||||
case ResourceType.dashboard:
|
||||
return 'dashboards';
|
||||
case ResourceType.alert:
|
||||
return 'alerts';
|
||||
case ResourceType.service:
|
||||
return 'apm';
|
||||
case ResourceType.saved_view:
|
||||
return 'savedViews';
|
||||
case ResourceType.logs_explorer:
|
||||
return 'logs';
|
||||
case ResourceType.traces_explorer:
|
||||
return 'traces';
|
||||
case ResourceType.metrics_explorer:
|
||||
return 'metrics';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps an apply_filter signal to its product module name. */
|
||||
function targetModuleForSignal(signal: ApplyFilterSignalDTO): string | null {
|
||||
switch (signal) {
|
||||
case ApplyFilterSignalDTO.logs:
|
||||
return 'logs';
|
||||
case ApplyFilterSignalDTO.traces:
|
||||
return 'traces';
|
||||
case ApplyFilterSignalDTO.metrics:
|
||||
return 'metrics';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
type ChipState = 'idle' | 'loading' | 'success' | 'error';
|
||||
@@ -94,23 +150,23 @@ function resourceRoute(
|
||||
resourceId: string,
|
||||
): string | null {
|
||||
switch (resourceType) {
|
||||
case 'dashboard':
|
||||
case ResourceType.dashboard:
|
||||
return ROUTES.DASHBOARD.replace(':dashboardId', resourceId);
|
||||
case 'alert': {
|
||||
case ResourceType.alert: {
|
||||
const params = new URLSearchParams({ [QueryParams.ruleId]: resourceId });
|
||||
return `${ROUTES.EDIT_ALERTS}?${params.toString()}`;
|
||||
}
|
||||
case 'service':
|
||||
case ResourceType.service:
|
||||
return ROUTES.SERVICE_METRICS.replace(':servicename', resourceId);
|
||||
case 'saved_view':
|
||||
case ResourceType.saved_view:
|
||||
// No detail route — saved views land on the list page.
|
||||
// Caller may provide signal-aware metadata in future; default to logs.
|
||||
return ROUTES.LOGS_SAVE_VIEWS;
|
||||
case 'logs_explorer':
|
||||
case ResourceType.logs_explorer:
|
||||
return ROUTES.LOGS_EXPLORER;
|
||||
case 'traces_explorer':
|
||||
case ResourceType.traces_explorer:
|
||||
return ROUTES.TRACES_EXPLORER;
|
||||
case 'metrics_explorer':
|
||||
case ResourceType.metrics_explorer:
|
||||
return ROUTES.METRICS_EXPLORER_EXPLORER;
|
||||
default:
|
||||
return null;
|
||||
@@ -224,6 +280,24 @@ function actionKey(action: MessageActionDTO, index: number): string {
|
||||
: `${action.kind}:${action.label}:${index}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the prompt to send for a follow_up action. The chip's `label` is
|
||||
* the short display text (e.g. "Python setup"); the real prompt lives in
|
||||
* `input.intent` per the schema doc. Falls back to label defensively so a
|
||||
* malformed server payload doesn't drop the click silently. Both branches
|
||||
* are trimmed so whitespace-only payloads don't become whitespace messages.
|
||||
*/
|
||||
function followUpIntent(action: MessageActionDTO): string {
|
||||
const intent = action.input?.intent;
|
||||
if (typeof intent === 'string') {
|
||||
const trimmed = intent.trim();
|
||||
if (trimmed.length > 0) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
return action.label.trim();
|
||||
}
|
||||
|
||||
/** Maps a signal to its target explorer route. */
|
||||
function explorerRouteForSignal(signal: ApplyFilterSignalDTO): string | null {
|
||||
switch (signal) {
|
||||
@@ -353,10 +427,12 @@ function rollbackCall(
|
||||
*/
|
||||
export default function ActionsSection({
|
||||
actions,
|
||||
messageId,
|
||||
}: ActionsSectionProps): JSX.Element | null {
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
|
||||
const { threadId, page, mode } = useAIAssistantAnalyticsContext();
|
||||
const { redirectWithQueryBuilderData, handleSetQueryData } = useQueryBuilder();
|
||||
|
||||
// Per-chip click state, keyed by chip key (see `key` below). Persists
|
||||
@@ -430,13 +506,39 @@ export default function ActionsSection({
|
||||
switch (action.kind) {
|
||||
case MessageActionKindDTO.open_docs: {
|
||||
if (action.url) {
|
||||
void logEvent(AIAssistantEvents.DocOpened, {
|
||||
threadId,
|
||||
messageId,
|
||||
docPath: action.url,
|
||||
});
|
||||
openInNewTab(action.url);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageActionKindDTO.follow_up: {
|
||||
if (action.label) {
|
||||
void sendMessage(action.label);
|
||||
const intent = followUpIntent(action);
|
||||
if (intent) {
|
||||
// Fire SuggestedPromptClicked + MessageSent so analytics can compute
|
||||
// both the click-through rate against follow-ups offered *and* keep
|
||||
// the unified send funnel intact. `category` distinguishes server-
|
||||
// emitted follow-ups from the empty-state grid. `promptId` stays the
|
||||
// label so dashboards group identical chip texts together regardless
|
||||
// of the dynamic intent payload.
|
||||
void logEvent(AIAssistantEvents.SuggestedPromptClicked, {
|
||||
threadId,
|
||||
messageId,
|
||||
promptId: action.label,
|
||||
category: SuggestedPromptCategory.FollowUp,
|
||||
});
|
||||
void logEvent(AIAssistantEvents.MessageSent, {
|
||||
threadId,
|
||||
page,
|
||||
mode,
|
||||
queryLength: intent.length,
|
||||
hasContext: false,
|
||||
respondingToClarification: false,
|
||||
});
|
||||
void sendMessage(intent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -444,6 +546,12 @@ export default function ActionsSection({
|
||||
if (action.resourceType && action.resourceId) {
|
||||
const path = resourceRoute(action.resourceType, action.resourceId);
|
||||
if (path) {
|
||||
void logEvent(AIAssistantEvents.ResourceOpened, {
|
||||
threadId,
|
||||
messageId,
|
||||
targetModule: targetModuleForResource(action.resourceType),
|
||||
resourceId: action.resourceId,
|
||||
});
|
||||
history.push(path);
|
||||
}
|
||||
}
|
||||
@@ -456,6 +564,13 @@ export default function ActionsSection({
|
||||
break;
|
||||
}
|
||||
case MessageActionKindDTO.apply_filter: {
|
||||
if (action.signal) {
|
||||
void logEvent(AIAssistantEvents.ApplyFilterClicked, {
|
||||
threadId,
|
||||
messageId,
|
||||
targetModule: targetModuleForSignal(action.signal),
|
||||
});
|
||||
}
|
||||
applyFilter(action, {
|
||||
history,
|
||||
pathname,
|
||||
|
||||
@@ -5,13 +5,17 @@ import { Badge } from '@signozhq/ui/badge';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import type { UploadFile } from 'antd';
|
||||
import getSessionStorage from 'api/browser/sessionstorage/get';
|
||||
import setSessionStorage from 'api/browser/sessionstorage/set';
|
||||
import {
|
||||
getListRulesQueryKey,
|
||||
useListRules,
|
||||
} from 'api/generated/services/rules';
|
||||
import type { ListRules200 } from 'api/generated/services/sigNoz.schemas';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||
import { useQueryService } from 'hooks/useQueryService';
|
||||
@@ -22,6 +26,8 @@ import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { AIAssistantEvents, getBrowserInfo } from '../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useSpeechRecognition } from '../../hooks/useSpeechRecognition';
|
||||
import { MessageAttachment } from '../../types';
|
||||
import { MessageContext } from '../../../../api/ai-assistant/chat';
|
||||
@@ -137,6 +143,8 @@ function autoContextCategory(ctx: MessageContext): string {
|
||||
const MAX_INPUT_LENGTH = 20000;
|
||||
const WARNING_THRESHOLD = 15000;
|
||||
const HOME_SERVICES_INTERVAL = 30 * 60 * 1000;
|
||||
/** sessionStorage key for the "voice input failed this tab" flag. */
|
||||
const VOICE_UNAVAILABLE_KEY = 'ai-assistant-voice-unavailable';
|
||||
|
||||
const CONTEXT_CATEGORIES = ['Dashboards', 'Alerts', 'Services'] as const;
|
||||
|
||||
@@ -368,6 +376,28 @@ export default function ChatInput({
|
||||
|
||||
// ── Voice input ────────────────────────────────────────────────────────────
|
||||
|
||||
const analyticsCtx = useAIAssistantAnalyticsContext();
|
||||
// Captured at the start of a voice session, consumed when it ends.
|
||||
// Tracks both the trigger (button vs. PTT shortcut) and the wall-clock
|
||||
// start time so we can attribute `durationMs` on the Voice input used
|
||||
// event regardless of which control ended the session.
|
||||
const voiceStartedAtRef = useRef<number | null>(null);
|
||||
const voiceSourceRef = useRef<'button' | 'shortcut' | null>(null);
|
||||
// Set to true after a `network`, `not-allowed`, or `not-supported` failure
|
||||
// so we hide the mic button for the rest of the tab session — silent
|
||||
// retries don't help, and Chromium derivatives without the Google Speech
|
||||
// API key always fail with `network` no matter how many times the user
|
||||
// clicks. Persisted to sessionStorage so a page reload doesn't surface the
|
||||
// button again (closing the tab still resets, in case the user fixed
|
||||
// permissions or switched browsers).
|
||||
const [voiceUnavailable, setVoiceUnavailable] = useState(
|
||||
() => getSessionStorage(VOICE_UNAVAILABLE_KEY) === 'true',
|
||||
);
|
||||
const markVoiceUnavailable = useCallback((): void => {
|
||||
setVoiceUnavailable(true);
|
||||
setSessionStorage(VOICE_UNAVAILABLE_KEY, 'true');
|
||||
}, []);
|
||||
|
||||
const {
|
||||
isListening,
|
||||
isSupported,
|
||||
@@ -388,9 +418,81 @@ export default function ChatInput({
|
||||
setText(capText(committedTextRef.current + separator + transcriptText));
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
// Guard against double-fire: Chrome can fire `onerror` more than
|
||||
// once per session when `continuous = true` (it retries internally
|
||||
// before giving up). Only fire the analytics event for the first
|
||||
// error in a given session — voiceSourceRef being null means we've
|
||||
// already handled it.
|
||||
const source = voiceSourceRef.current;
|
||||
if (source === null) {
|
||||
return;
|
||||
}
|
||||
voiceStartedAtRef.current = null;
|
||||
voiceSourceRef.current = null;
|
||||
void logEvent(AIAssistantEvents.VoiceInputFailed, {
|
||||
...analyticsCtx,
|
||||
...getBrowserInfo(),
|
||||
source,
|
||||
errorType: error,
|
||||
});
|
||||
if (error === 'network') {
|
||||
markVoiceUnavailable();
|
||||
toast.error('Voice input unavailable in this browser', {
|
||||
description:
|
||||
'This browser cannot reach the speech recognition service. Try Google Chrome or Microsoft Edge.',
|
||||
});
|
||||
} else if (error === 'not-allowed') {
|
||||
markVoiceUnavailable();
|
||||
toast.error('Microphone access denied', {
|
||||
description:
|
||||
'Grant microphone permission in your browser settings to use voice input.',
|
||||
});
|
||||
} else if (error === 'not-supported') {
|
||||
markVoiceUnavailable();
|
||||
toast.error('Voice input is not supported in this browser.');
|
||||
}
|
||||
// `no-speech` is benign (just silence) — don't toast or hide.
|
||||
},
|
||||
});
|
||||
|
||||
const showMic = isSupported && micPermission !== 'denied';
|
||||
const showMic = isSupported && micPermission !== 'denied' && !voiceUnavailable;
|
||||
|
||||
const startVoiceInput = useCallback(
|
||||
(source: 'button' | 'shortcut') => {
|
||||
// Defense in depth: the button is hidden when `voiceUnavailable` is
|
||||
// true, but the PTT shortcut listener can still call us. Bailing here
|
||||
// keeps a single source of truth and prevents repeat `Voice input
|
||||
// failed` events in the same session.
|
||||
if (voiceUnavailable) {
|
||||
return;
|
||||
}
|
||||
voiceStartedAtRef.current = Date.now();
|
||||
voiceSourceRef.current = source;
|
||||
start();
|
||||
},
|
||||
[start, voiceUnavailable],
|
||||
);
|
||||
|
||||
const fireVoiceInputEvent = useCallback(
|
||||
(outcome: 'sent' | 'discarded') => {
|
||||
const startedAt = voiceStartedAtRef.current;
|
||||
const source = voiceSourceRef.current;
|
||||
voiceStartedAtRef.current = null;
|
||||
voiceSourceRef.current = null;
|
||||
if (startedAt === null || source === null) {
|
||||
return;
|
||||
}
|
||||
void logEvent(AIAssistantEvents.VoiceInputUsed, {
|
||||
...analyticsCtx,
|
||||
...getBrowserInfo(),
|
||||
source,
|
||||
outcome,
|
||||
durationMs: Date.now() - startedAt,
|
||||
});
|
||||
},
|
||||
[analyticsCtx],
|
||||
);
|
||||
|
||||
// Stop recording and immediately send whatever is in the textarea.
|
||||
const handleStopAndSend = useCallback(async () => {
|
||||
@@ -398,15 +500,17 @@ export default function ChatInput({
|
||||
committedTextRef.current = capText(text);
|
||||
// Stop recognition without triggering onTranscript again (would double-append).
|
||||
discard();
|
||||
fireVoiceInputEvent('sent');
|
||||
await handleSend();
|
||||
}, [text, discard, handleSend, capText]);
|
||||
}, [text, discard, handleSend, capText, fireVoiceInputEvent]);
|
||||
|
||||
// Stop recording and revert the textarea to what it was before voice started.
|
||||
const handleDiscard = useCallback(() => {
|
||||
discard();
|
||||
fireVoiceInputEvent('discarded');
|
||||
setText(committedTextRef.current);
|
||||
textareaRef.current?.focus();
|
||||
}, [discard]);
|
||||
}, [discard, fireVoiceInputEvent]);
|
||||
|
||||
// ── Push-to-talk (Cmd/Ctrl + Shift + Space) ────────────────────────────────
|
||||
// Hold the combo to record; release Space to submit. We track which key
|
||||
@@ -415,7 +519,7 @@ export default function ChatInput({
|
||||
// "session active" ref so a held key only calls `start()` once.
|
||||
const pttActiveRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!isSupported || micPermission === 'denied') {
|
||||
if (!isSupported || micPermission === 'denied' || voiceUnavailable) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -432,7 +536,7 @@ export default function ChatInput({
|
||||
return; // ignore auto-repeat
|
||||
}
|
||||
pttActiveRef.current = true;
|
||||
start();
|
||||
startVoiceInput('shortcut');
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent): void => {
|
||||
@@ -466,9 +570,10 @@ export default function ChatInput({
|
||||
}, [
|
||||
isSupported,
|
||||
micPermission,
|
||||
voiceUnavailable,
|
||||
disabled,
|
||||
isStreaming,
|
||||
start,
|
||||
startVoiceInput,
|
||||
handleStopAndSend,
|
||||
]);
|
||||
|
||||
@@ -903,7 +1008,7 @@ export default function ChatInput({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={start}
|
||||
onClick={(): void => startVoiceInput('button')}
|
||||
disabled={disabled}
|
||||
aria-label="Start voice input"
|
||||
className={styles.micBtn}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from '@signozhq/ui/select';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ClarificationFieldTypeDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
|
||||
import type {
|
||||
ClarificationEventDTO,
|
||||
@@ -16,6 +17,8 @@ import type {
|
||||
} from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
|
||||
import { CircleHelp, Send, X } from '@signozhq/icons';
|
||||
|
||||
import { AIAssistantEvents } from '../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
|
||||
|
||||
import styles from './ClarificationForm.module.scss';
|
||||
@@ -44,6 +47,8 @@ export default function ClarificationForm({
|
||||
const isStreaming = useAIAssistantStore(
|
||||
(s) => s.streams[conversationId]?.isStreaming ?? false,
|
||||
);
|
||||
const { threadId, page, mode } =
|
||||
useAIAssistantAnalyticsContext(conversationId);
|
||||
|
||||
const fields = clarification.fields ?? [];
|
||||
const initialAnswers = Object.fromEntries(
|
||||
@@ -60,6 +65,18 @@ export default function ClarificationForm({
|
||||
|
||||
const handleSubmit = async (): Promise<void> => {
|
||||
setSubmitted(true);
|
||||
// Approximate queryLength as the JSON encoding of the form answers — the
|
||||
// clarification API doesn't render a single user-visible string, but the
|
||||
// JSON size is a reasonable stand-in for "how much did the user provide".
|
||||
const queryLength = JSON.stringify(answers).length;
|
||||
void logEvent(AIAssistantEvents.MessageSent, {
|
||||
threadId,
|
||||
page,
|
||||
mode,
|
||||
queryLength,
|
||||
hasContext: false,
|
||||
respondingToClarification: true,
|
||||
});
|
||||
await submitClarification(
|
||||
conversationId,
|
||||
clarification.clarificationId,
|
||||
@@ -69,6 +86,10 @@ export default function ClarificationForm({
|
||||
|
||||
const handleCancel = (): void => {
|
||||
setCancelled(true);
|
||||
void logEvent(AIAssistantEvents.CancelClicked, {
|
||||
threadId,
|
||||
secondsSinceStart: null,
|
||||
});
|
||||
cancelStream(conversationId);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ import { Input } from '@signozhq/ui/input';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Plus, Search } from '@signozhq/icons';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import { AIAssistantEvents } from '../../events';
|
||||
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
|
||||
import { Conversation } from '../../types';
|
||||
import { useVariant } from '../../VariantContext';
|
||||
@@ -136,6 +139,17 @@ export default function ConversationsList({
|
||||
|
||||
const handleSelect = (id: string): void => {
|
||||
const conv = conversations[id];
|
||||
// Skip re-selecting the currently active thread — Notion-style click on
|
||||
// the highlighted row in the history list shouldn't inflate the funnel.
|
||||
const isReselectingActive = id === activeConversationId;
|
||||
if (conv?.threadId && !isReselectingActive) {
|
||||
void logEvent(AIAssistantEvents.ThreadOpenedFromHistory, {
|
||||
threadId: conv.threadId,
|
||||
threadAgeDays: Math.floor(
|
||||
(Date.now() - conv.createdAt) / (24 * 60 * 60 * 1000),
|
||||
),
|
||||
});
|
||||
}
|
||||
if (conv?.threadId) {
|
||||
// Always load from backend — refreshes messages and reconnects
|
||||
// to active execution if the thread is still busy.
|
||||
|
||||
@@ -144,7 +144,7 @@ export default function MessageBubble({
|
||||
)}
|
||||
|
||||
{!isUser && message.actions && message.actions.length > 0 && (
|
||||
<ActionsSection actions={message.actions} />
|
||||
<ActionsSection actions={message.actions} messageId={message.id} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,10 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { Check, Copy, RefreshCw, ThumbsDown, ThumbsUp } from '@signozhq/icons';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import { AIAssistantEvents } from '../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
|
||||
import { FeedbackRating, Message } from '../../types';
|
||||
|
||||
@@ -54,6 +58,7 @@ export default function MessageFeedback({
|
||||
const submitMessageFeedback = useAIAssistantStore(
|
||||
(s) => s.submitMessageFeedback,
|
||||
);
|
||||
const { threadId } = useAIAssistantAnalyticsContext();
|
||||
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
@@ -91,10 +96,21 @@ export default function MessageFeedback({
|
||||
}, [message.createdAt]);
|
||||
|
||||
const handleCopy = useCallback((): void => {
|
||||
void logEvent(AIAssistantEvents.MessageCopied, {
|
||||
role: message.role,
|
||||
messageId: message.id,
|
||||
hadToolCalls: Boolean(message.blocks?.some((b) => b.type === 'tool_call')),
|
||||
});
|
||||
copyToClipboard(message.content);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
}, [copyToClipboard, message.content]);
|
||||
}, [
|
||||
copyToClipboard,
|
||||
message.content,
|
||||
message.id,
|
||||
message.role,
|
||||
message.blocks,
|
||||
]);
|
||||
|
||||
const handleVote = useCallback(
|
||||
(rating: FeedbackRating): void => {
|
||||
@@ -107,20 +123,31 @@ export default function MessageFeedback({
|
||||
return;
|
||||
}
|
||||
setVote(rating);
|
||||
void logEvent(AIAssistantEvents.FeedbackSubmitted, {
|
||||
messageId: message.id,
|
||||
threadId,
|
||||
rating: 'up',
|
||||
hasComment: false,
|
||||
commentLength: 0,
|
||||
});
|
||||
submitMessageFeedback(message.id, rating);
|
||||
},
|
||||
[vote, message.id, submitMessageFeedback],
|
||||
[vote, message.id, submitMessageFeedback, threadId],
|
||||
);
|
||||
|
||||
const handleSubmitNegative = useCallback((): void => {
|
||||
setVote('negative');
|
||||
setIsNegativeDialogOpen(false);
|
||||
submitMessageFeedback(
|
||||
message.id,
|
||||
'negative',
|
||||
negativeComment.trim() || undefined,
|
||||
);
|
||||
}, [message.id, negativeComment, submitMessageFeedback]);
|
||||
const trimmed = negativeComment.trim();
|
||||
void logEvent(AIAssistantEvents.FeedbackSubmitted, {
|
||||
messageId: message.id,
|
||||
threadId,
|
||||
rating: 'down',
|
||||
hasComment: trimmed.length > 0,
|
||||
commentLength: trimmed.length,
|
||||
});
|
||||
submitMessageFeedback(message.id, 'negative', trimmed || undefined);
|
||||
}, [message.id, negativeComment, submitMessageFeedback, threadId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -4,6 +4,9 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Check, Copy } from '@signozhq/icons';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import { AIAssistantEvents } from '../../events';
|
||||
import { Message } from '../../types';
|
||||
|
||||
import styles from './UserMessageActions.module.scss';
|
||||
@@ -25,10 +28,15 @@ export default function UserMessageActions({
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const handleCopy = useCallback((): void => {
|
||||
void logEvent(AIAssistantEvents.MessageCopied, {
|
||||
role: message.role,
|
||||
messageId: message.id,
|
||||
hadToolCalls: false,
|
||||
});
|
||||
copyToClipboard(message.content);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
}, [copyToClipboard, message.content]);
|
||||
}, [copyToClipboard, message.content, message.id, message.role]);
|
||||
|
||||
return (
|
||||
<div className={styles.actions}>
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
Sparkles,
|
||||
} from '@signozhq/icons';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import { AIAssistantEvents, SuggestedPromptCategory } from '../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
|
||||
import { Message, StreamingEventItem } from '../../types';
|
||||
import MessageBubble from '../MessageBubble';
|
||||
@@ -46,17 +50,24 @@ interface VirtualizedMessagesProps {
|
||||
conversationId: string;
|
||||
messages: Message[];
|
||||
isStreaming: boolean;
|
||||
/**
|
||||
* Called when a user clicks an empty-state suggested prompt. Routed
|
||||
* through the parent so analytics (Message sent) fire with the same
|
||||
* page/mode/context attribution as a normal send.
|
||||
*/
|
||||
onSendSuggestedPrompt: (text: string) => void;
|
||||
}
|
||||
|
||||
export default function VirtualizedMessages({
|
||||
conversationId,
|
||||
messages,
|
||||
isStreaming,
|
||||
onSendSuggestedPrompt,
|
||||
}: VirtualizedMessagesProps): JSX.Element {
|
||||
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
|
||||
const regenerateAssistantMessage = useAIAssistantStore(
|
||||
(s) => s.regenerateAssistantMessage,
|
||||
);
|
||||
const { threadId } = useAIAssistantAnalyticsContext(conversationId);
|
||||
const streamingStatus = useAIAssistantStore(
|
||||
(s) => s.streams[conversationId]?.streamingStatus ?? '',
|
||||
);
|
||||
@@ -85,9 +96,13 @@ export default function VirtualizedMessages({
|
||||
if (isStreaming) {
|
||||
return;
|
||||
}
|
||||
void logEvent(AIAssistantEvents.RegenerateClicked, {
|
||||
messageId,
|
||||
threadId,
|
||||
});
|
||||
void regenerateAssistantMessage(conversationId, messageId);
|
||||
},
|
||||
[conversationId, isStreaming, regenerateAssistantMessage],
|
||||
[conversationId, isStreaming, regenerateAssistantMessage, threadId],
|
||||
);
|
||||
|
||||
// Scroll all the way to the actual bottom — including the 64px of bottom
|
||||
@@ -146,7 +161,11 @@ export default function VirtualizedMessages({
|
||||
color="secondary"
|
||||
className={styles.emptyChip}
|
||||
onClick={(): void => {
|
||||
sendMessage(s.text);
|
||||
void logEvent(AIAssistantEvents.SuggestedPromptClicked, {
|
||||
promptId: s.text,
|
||||
category: SuggestedPromptCategory.EmptyState,
|
||||
});
|
||||
onSendSuggestedPrompt(s.text);
|
||||
}}
|
||||
prefix={<s.icon size={14} />}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import cx from 'classnames';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { Check, X } from '@signozhq/icons';
|
||||
|
||||
import { AIAssistantEvents } from '../../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../../../store/useAIAssistantStore';
|
||||
import { useMessageContext } from '../../MessageContext';
|
||||
|
||||
@@ -37,6 +40,7 @@ export default function ConfirmBlock({
|
||||
const answeredBlocks = useAIAssistantStore((s) => s.answeredBlocks);
|
||||
const markBlockAnswered = useAIAssistantStore((s) => s.markBlockAnswered);
|
||||
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
|
||||
const { threadId, page, mode } = useAIAssistantAnalyticsContext();
|
||||
|
||||
// Durable answered state — survives re-renders/remounts
|
||||
const answeredChoice = messageId ? answeredBlocks[messageId] : undefined;
|
||||
@@ -47,6 +51,14 @@ export default function ConfirmBlock({
|
||||
if (messageId) {
|
||||
markBlockAnswered(messageId, choice);
|
||||
}
|
||||
void logEvent(AIAssistantEvents.MessageSent, {
|
||||
threadId,
|
||||
page,
|
||||
mode,
|
||||
queryLength: responseText.length,
|
||||
hasContext: false,
|
||||
respondingToClarification: false,
|
||||
});
|
||||
sendMessage(responseText);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { Checkbox, Radio } from 'antd';
|
||||
|
||||
import { AIAssistantEvents } from '../../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../../../store/useAIAssistantStore';
|
||||
import { useMessageContext } from '../../MessageContext';
|
||||
|
||||
@@ -36,6 +39,7 @@ export default function InteractiveQuestion({
|
||||
const answeredBlocks = useAIAssistantStore((s) => s.answeredBlocks);
|
||||
const markBlockAnswered = useAIAssistantStore((s) => s.markBlockAnswered);
|
||||
const sendMessage = useAIAssistantStore((s) => s.sendMessage);
|
||||
const { threadId, page, mode } = useAIAssistantAnalyticsContext();
|
||||
|
||||
// Persist selected state locally only for the pending (not-yet-submitted) case
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
@@ -52,6 +56,14 @@ export default function InteractiveQuestion({
|
||||
if (messageId) {
|
||||
markBlockAnswered(messageId, answer);
|
||||
}
|
||||
void logEvent(AIAssistantEvents.MessageSent, {
|
||||
threadId,
|
||||
page,
|
||||
mode,
|
||||
queryLength: answer.length,
|
||||
hasContext: false,
|
||||
respondingToClarification: false,
|
||||
});
|
||||
sendMessage(answer);
|
||||
};
|
||||
|
||||
|
||||
81
frontend/src/container/AIAssistant/events.ts
Normal file
81
frontend/src/container/AIAssistant/events.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Analytics event names for the AI Assistant feature. Backend-emitted events
|
||||
* (Execution finished, Approval resolved, Resource mutated, Clarification
|
||||
* requested, Limit hit) are not declared here — they fire from the AI service.
|
||||
*/
|
||||
|
||||
export interface BrowserInfo {
|
||||
browserName: string;
|
||||
browserVersion: string;
|
||||
}
|
||||
|
||||
type NavigatorWithBrandHints = Navigator & {
|
||||
userAgentData?: { brands: { brand: string; version: string }[] };
|
||||
brave?: { isBrave: () => Promise<boolean> };
|
||||
};
|
||||
|
||||
/**
|
||||
* We mainly need to distinguish Chrome / Edge (Speech API works) from Chromium
|
||||
* derivatives (no Google API key → voice fails with `network`). UA sniffing is
|
||||
* the source of truth for derivative identification; `userAgentData` is used
|
||||
* only as a fast happy path for Chrome / Edge. Brave needs its own probe — it
|
||||
* advertises Chrome in both UA and brand hints.
|
||||
*/
|
||||
export function getBrowserInfo(): BrowserInfo {
|
||||
if (typeof navigator === 'undefined') {
|
||||
return { browserName: 'unknown', browserVersion: 'unknown' };
|
||||
}
|
||||
const nav = navigator as NavigatorWithBrandHints;
|
||||
const ua = nav.userAgent;
|
||||
|
||||
// Order matters: derivatives put "Chrome" in their UA; Chrome puts "Safari".
|
||||
const matchers: { name: string; re: RegExp }[] = [
|
||||
{ name: 'Edge', re: /Edg(?:e|A|iOS)?\/([\d.]+)/ },
|
||||
{ name: 'Opera', re: /OPR\/([\d.]+)/ },
|
||||
{ name: 'Vivaldi', re: /Vivaldi\/([\d.]+)/ },
|
||||
{ name: 'Chrome', re: /Chrome\/([\d.]+)/ },
|
||||
{ name: 'Firefox', re: /Firefox\/([\d.]+)/ },
|
||||
{ name: 'Safari', re: /Version\/([\d.]+).*Safari/ },
|
||||
];
|
||||
let browserName = 'unknown';
|
||||
let browserVersion = 'unknown';
|
||||
for (const { name, re } of matchers) {
|
||||
const m = ua.match(re);
|
||||
if (m) {
|
||||
browserName = name;
|
||||
browserVersion = m[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Brave hides as Chrome in UA + brand hints; its probe is the only tell.
|
||||
if (nav.brave?.isBrave) {
|
||||
browserName = 'Brave';
|
||||
}
|
||||
|
||||
return { browserName, browserVersion };
|
||||
}
|
||||
|
||||
export const SuggestedPromptCategory = {
|
||||
FollowUp: 'follow_up',
|
||||
EmptyState: 'empty_state',
|
||||
} as const;
|
||||
export type SuggestedPromptCategory =
|
||||
(typeof SuggestedPromptCategory)[keyof typeof SuggestedPromptCategory];
|
||||
|
||||
export enum AIAssistantEvents {
|
||||
Opened = 'AI Assistant: Opened',
|
||||
MessageSent = 'AI Assistant: Message sent',
|
||||
SuggestedPromptClicked = 'AI Assistant: Suggested prompt clicked',
|
||||
CancelClicked = 'AI Assistant: Cancel clicked',
|
||||
RegenerateClicked = 'AI Assistant: Regenerate clicked',
|
||||
MessageCopied = 'AI Assistant: Message copied',
|
||||
FeedbackSubmitted = 'AI Assistant: Feedback submitted',
|
||||
ResourceOpened = 'AI Assistant: Resource opened',
|
||||
DocOpened = 'AI Assistant: Doc opened',
|
||||
ApplyFilterClicked = 'AI Assistant: Apply filter clicked',
|
||||
ThreadOpenedFromHistory = 'AI Assistant: Thread opened from history',
|
||||
VoiceInputUsed = 'AI Assistant: Voice input used',
|
||||
VoiceInputFailed = 'AI Assistant: Voice input failed',
|
||||
NewChatClicked = 'AI Assistant: New chat clicked',
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
import { useAIAssistantStore } from '../store/useAIAssistantStore';
|
||||
import { useVariant } from '../VariantContext';
|
||||
|
||||
export interface AIAssistantAnalyticsContext {
|
||||
/** Backend thread ID for the resolved conversation; undefined before the first send. */
|
||||
threadId: string | undefined;
|
||||
/**
|
||||
* Normalised route template for the current page (e.g. `/dashboard/:dashboardId`).
|
||||
* Falls back to the raw pathname for routes not in ROUTES. We normalise to keep
|
||||
* analytics cardinality bounded and avoid leaking customer identifiers
|
||||
* (dashboard IDs, service names, trace IDs, conversation IDs) into the event.
|
||||
*/
|
||||
page: string;
|
||||
/** Surface the assistant is rendered on. `panel` / `modal` collapse to `sidepane`. */
|
||||
mode: 'sidepane' | 'full_screen';
|
||||
}
|
||||
|
||||
// Pre-sorted longest-first so more specific templates match before their
|
||||
// less specific siblings (e.g. `/services/:s/top-level-operations` wins
|
||||
// over `/services/:s`). Module-level — ROUTES is static.
|
||||
const ROUTE_TEMPLATES = Object.values(ROUTES).sort(
|
||||
(a, b) => b.length - a.length,
|
||||
);
|
||||
|
||||
export function normalizePage(pathname: string): string {
|
||||
for (const template of ROUTE_TEMPLATES) {
|
||||
if (matchPath(pathname, { path: template, exact: true })) {
|
||||
return template;
|
||||
}
|
||||
}
|
||||
return pathname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared base attributes for AI Assistant analytics events (Message sent,
|
||||
* Cancel clicked, Feedback submitted, Resource/Doc/Apply filter, …).
|
||||
*
|
||||
* Pass `conversationId` when the caller is scoped to a specific
|
||||
* conversation (e.g. `ClarificationForm`, `VirtualizedMessages`); omit
|
||||
* to fall back to the store's active conversation.
|
||||
*/
|
||||
export function useAIAssistantAnalyticsContext(
|
||||
conversationId?: string,
|
||||
): AIAssistantAnalyticsContext {
|
||||
const { pathname } = useLocation();
|
||||
const variant = useVariant();
|
||||
const threadId = useAIAssistantStore((s) => {
|
||||
const id = conversationId ?? s.activeConversationId;
|
||||
return id ? s.conversations[id]?.threadId : undefined;
|
||||
});
|
||||
return {
|
||||
threadId,
|
||||
page: normalizePage(pathname),
|
||||
mode: variant === 'page' ? 'full_screen' : 'sidepane',
|
||||
};
|
||||
}
|
||||
@@ -1,12 +1,23 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import * as usePrefillAlertConditions from 'container/FormAlertRules/usePrefillAlertConditions';
|
||||
import AlertTypeSelectionPage from 'pages/AlertTypeSelection';
|
||||
import CreateAlertPage from 'pages/CreateAlert';
|
||||
import { act, fireEvent, render } from 'tests/test-utils';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { ALERT_TYPE_TO_TITLE, ALERT_TYPE_URL_MAP } from './constants';
|
||||
|
||||
jest.mock('react-router-dom-v5-compat', () => ({
|
||||
...jest.requireActual('react-router-dom-v5-compat'),
|
||||
useNavigationType: jest.fn(() => 'PUSH'),
|
||||
useLocation: jest.fn(() => ({
|
||||
pathname: '/alerts/new',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
})),
|
||||
useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]),
|
||||
}));
|
||||
|
||||
jest
|
||||
.spyOn(usePrefillAlertConditions, 'usePrefillAlertConditions')
|
||||
.mockReturnValue({
|
||||
@@ -54,20 +65,13 @@ describe('Alert rule documentation redirection', () => {
|
||||
window.open = mockWindowOpen;
|
||||
});
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALERT_TYPE_SELECTION}`,
|
||||
}),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
renderResult = render(
|
||||
<AlertTypeSelectionPage />,
|
||||
<CreateAlertPage />,
|
||||
{},
|
||||
{
|
||||
initialRoute: ROUTES.ALERT_TYPE_SELECTION,
|
||||
initialRoute: ROUTES.ALERTS_NEW,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,6 +15,18 @@ jest.mock('react-router-dom', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom-v5-compat', () => ({
|
||||
...jest.requireActual('react-router-dom-v5-compat'),
|
||||
useNavigationType: jest.fn(() => 'PUSH'),
|
||||
useLocation: jest.fn(() => ({
|
||||
pathname: '/alerts/new',
|
||||
search: 'ruleType=anomaly_rule',
|
||||
hash: '',
|
||||
state: null,
|
||||
})),
|
||||
useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]),
|
||||
}));
|
||||
|
||||
window.ResizeObserver =
|
||||
window.ResizeObserver ||
|
||||
jest.fn().mockImplementation(() => ({
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
.create-alert-tabs {
|
||||
&__extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.create-alert-wrapper {
|
||||
margin-top: 10px;
|
||||
|
||||
.divider {
|
||||
border-color: var(--l1-border);
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.breadcrumb-divider {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.create-alert__breadcrumb {
|
||||
padding-left: 16px;
|
||||
|
||||
ol {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.25px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ant-breadcrumb-separator,
|
||||
.breadcrumb-item--last {
|
||||
color: var(--muted-foreground);
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
}
|
||||
|
||||
.alerts-container {
|
||||
.top-level-tab.periscope-tab {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.ant-tabs {
|
||||
&-nav {
|
||||
padding: 0 8px;
|
||||
margin-bottom: 0 !important;
|
||||
|
||||
&::before {
|
||||
border-bottom: 1px solid var(--l1-border) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-tab {
|
||||
&[data-node-key='TriggeredAlerts'] {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-left: 24px !important;
|
||||
}
|
||||
|
||||
[aria-selected='false'] {
|
||||
.periscope-tab {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
|
||||
import * as appHooks from 'providers/App/App';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import SelectAlertType from '..';
|
||||
|
||||
const useAppContextSpy = jest.spyOn(appHooks, 'useAppContext');
|
||||
|
||||
describe('SelectAlertType', () => {
|
||||
const mockOnSelect = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useAppContextSpy.mockReturnValue(getAppContextMockState());
|
||||
});
|
||||
|
||||
it('should render all alert type options when anomaly detection is enabled', () => {
|
||||
useAppContextSpy.mockReturnValue({
|
||||
...getAppContextMockState({}),
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.ANOMALY_DETECTION,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
|
||||
expect(screen.getByText('metric_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('log_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('traces_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('exceptions_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('anomaly_based_alert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render all alert type options except anomaly based alert when anomaly detection is disabled', () => {
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
|
||||
expect(screen.getByText('metric_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('log_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('traces_based_alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('exceptions_based_alert')).toBeInTheDocument();
|
||||
expect(screen.queryByText('anomaly_based_alert')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onSelect with metrics based alert type', () => {
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
fireEvent.click(screen.getByText('metric_based_alert'));
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onSelect with anomaly based alert type', () => {
|
||||
useAppContextSpy.mockReturnValue({
|
||||
...getAppContextMockState({}),
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.ANOMALY_DETECTION,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
fireEvent.click(screen.getByText('anomaly_based_alert'));
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
AlertTypes.ANOMALY_BASED_ALERT,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onSelect with log based alert type', () => {
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
fireEvent.click(screen.getByText('log_based_alert'));
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(AlertTypes.LOGS_BASED_ALERT, false);
|
||||
});
|
||||
|
||||
it('should call onSelect with traces based alert type', () => {
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
fireEvent.click(screen.getByText('traces_based_alert'));
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
AlertTypes.TRACES_BASED_ALERT,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onSelect with exceptions based alert type', () => {
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
fireEvent.click(screen.getByText('exceptions_based_alert'));
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
AlertTypes.EXCEPTIONS_BASED_ALERT,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,37 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
|
||||
import * as useCompositeQueryParamHooks from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import * as navigateHooks from 'hooks/useSafeNavigate';
|
||||
import * as useUrlQueryHooks from 'hooks/useUrlQuery';
|
||||
import * as appHooks from 'providers/App/App';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import CreateAlertRule from '../index';
|
||||
|
||||
jest.mock('react-router-dom-v5-compat', () => ({
|
||||
...jest.requireActual('react-router-dom-v5-compat'),
|
||||
useNavigationType: jest.fn(() => 'PUSH'),
|
||||
useLocation: jest.fn(() => ({
|
||||
pathname: '/alerts/new',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
})),
|
||||
useSearchParams: jest.fn(() => [new URLSearchParams(), jest.fn()]),
|
||||
}));
|
||||
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
|
||||
__esModule: true,
|
||||
default: function MockDateTimeSelector(): JSX.Element {
|
||||
return <div data-testid="datetime-selector">Mock DateTime Selector</div>;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('container/FormAlertRules', () => ({
|
||||
__esModule: true,
|
||||
default: function MockFormAlertRules({
|
||||
@@ -48,10 +72,14 @@ const useCompositeQueryParamSpy = jest.spyOn(
|
||||
'useGetCompositeQueryParam',
|
||||
);
|
||||
const useUrlQuerySpy = jest.spyOn(useUrlQueryHooks, 'default');
|
||||
const useSafeNavigateSpy = jest.spyOn(navigateHooks, 'useSafeNavigate');
|
||||
const useAppContextSpy = jest.spyOn(appHooks, 'useAppContext');
|
||||
|
||||
const mockSetUrlQuery = jest.fn();
|
||||
const mockToString = jest.fn();
|
||||
const mockGetUrlQuery = jest.fn();
|
||||
const mockSafeNavigate = jest.fn();
|
||||
const mockDeleteUrlQuery = jest.fn();
|
||||
|
||||
const FORM_ALERT_RULES_TEXT = 'Form Alert Rules';
|
||||
const CREATE_ALERT_V2_TEXT = 'Create Alert V2';
|
||||
@@ -63,8 +91,13 @@ describe('CreateAlertRule', () => {
|
||||
set: mockSetUrlQuery,
|
||||
toString: mockToString,
|
||||
get: mockGetUrlQuery,
|
||||
delete: mockDeleteUrlQuery,
|
||||
} as Partial<URLSearchParams> as URLSearchParams);
|
||||
useCompositeQueryParamSpy.mockReturnValue(initialQueriesMap.metrics);
|
||||
useSafeNavigateSpy.mockReturnValue({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
});
|
||||
useAppContextSpy.mockReturnValue(getAppContextMockState());
|
||||
});
|
||||
|
||||
it('should render classic flow when showClassicCreateAlertsPage is true', () => {
|
||||
@@ -72,18 +105,53 @@ describe('CreateAlertRule', () => {
|
||||
if (key === QueryParams.showClassicCreateAlertsPage) {
|
||||
return 'true';
|
||||
}
|
||||
if (key === QueryParams.alertType) {
|
||||
return AlertTypes.METRICS_BASED_ALERT;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render new flow by default', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
it('should render new flow when alertType is provided', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.alertType) {
|
||||
return AlertTypes.METRICS_BASED_ALERT;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render type selection when no alertType in URL and no compositeQuery', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
useCompositeQueryParamSpy.mockReturnValue(null);
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.queryByText(FORM_ALERT_RULES_TEXT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(CREATE_ALERT_V2_TEXT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should skip type selection and render alert form when compositeQuery is present', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
useCompositeQueryParamSpy.mockReturnValue({
|
||||
...initialQueriesMap.metrics,
|
||||
builder: {
|
||||
...initialQueriesMap.metrics.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueriesMap.metrics.builder.queryData[0],
|
||||
dataSource: DataSource.METRICS,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.METRICS_BASED_ALERT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render classic flow when ruleType is anomaly_rule even if showClassicCreateAlertsPage is not true', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.showClassicCreateAlertsPage) {
|
||||
@@ -111,8 +179,13 @@ describe('CreateAlertRule', () => {
|
||||
expect(screen.getByText(AlertTypes.LOGS_BASED_ALERT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use alertType from compositeQuery dataSource when alertType is not in URL', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
it('should use alertType from URL over compositeQuery dataSource', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.alertType) {
|
||||
return AlertTypes.LOGS_BASED_ALERT;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
useCompositeQueryParamSpy.mockReturnValue({
|
||||
...initialQueriesMap.metrics,
|
||||
builder: {
|
||||
@@ -127,14 +200,123 @@ describe('CreateAlertRule', () => {
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.TRACES_BASED_ALERT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.LOGS_BASED_ALERT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should default to METRICS_BASED_ALERT when no alertType and no compositeQuery', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
useCompositeQueryParamSpy.mockReturnValue(null);
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.METRICS_BASED_ALERT)).toBeInTheDocument();
|
||||
describe('handleSelectType navigation', () => {
|
||||
beforeEach(() => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
useCompositeQueryParamSpy.mockReturnValue(null);
|
||||
});
|
||||
|
||||
it('should navigate with threshold alert params for metrics alert', () => {
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('metric_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'threshold_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate with threshold alert params for logs alert', () => {
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('log_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'threshold_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.LOGS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate with threshold alert params for traces alert', () => {
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('traces_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'threshold_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.TRACES_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate with threshold alert params for exceptions alert', () => {
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('exceptions_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'threshold_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.EXCEPTIONS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate with anomaly detection params for anomaly alert', () => {
|
||||
useAppContextSpy.mockReturnValue({
|
||||
...getAppContextMockState({}),
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.ANOMALY_DETECTION,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('anomaly_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'anomaly_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate even when showClassicCreateAlertsPage flag is present', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.showClassicCreateAlertsPage) {
|
||||
return 'true';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
render(<CreateAlertRule />);
|
||||
fireEvent.click(screen.getByText('metric_based_alert'));
|
||||
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleType,
|
||||
'threshold_rule',
|
||||
);
|
||||
expect(mockSetUrlQuery).toHaveBeenCalledWith(
|
||||
QueryParams.alertType,
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,3 +208,11 @@ export const ALERTS_VALUES_MAP: Record<AlertTypes, AlertDef> = {
|
||||
[AlertTypes.TRACES_BASED_ALERT]: traceAlertDefaults,
|
||||
[AlertTypes.EXCEPTIONS_BASED_ALERT]: exceptionAlertDefaults,
|
||||
};
|
||||
|
||||
export const ALERT_TYPE_BREADCRUMB_TITLE: Record<AlertTypes, string> = {
|
||||
[AlertTypes.ANOMALY_BASED_ALERT]: 'Anomaly-Based Alert',
|
||||
[AlertTypes.METRICS_BASED_ALERT]: 'Metric-Based Alert',
|
||||
[AlertTypes.LOGS_BASED_ALERT]: 'Log-Based Alert',
|
||||
[AlertTypes.TRACES_BASED_ALERT]: 'Traces-Based Alert',
|
||||
[AlertTypes.EXCEPTIONS_BASED_ALERT]: 'Exceptions-Based Alert',
|
||||
};
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Form } from 'antd';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Form, Tabs, TabsProps } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon';
|
||||
import AlertBreadcrumb from 'components/AlertBreadcrumb';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import CreateAlertV2 from 'container/CreateAlertV2';
|
||||
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { AlertListTabs } from 'pages/AlertList/types';
|
||||
import { GalleryVerticalEnd, Pyramid } from '@signozhq/icons';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
|
||||
import { ALERTS_VALUES_MAP } from './defaults';
|
||||
import { ALERTS_VALUES_MAP, ALERT_TYPE_BREADCRUMB_TITLE } from './defaults';
|
||||
import SelectAlertType from './SelectAlertType';
|
||||
|
||||
import './CreateAlertRule.styles.scss';
|
||||
|
||||
function CreateRules(): JSX.Element {
|
||||
const [formInstance] = Form.useForm();
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
const queryParams = useUrlQuery();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const ruleTypeFromURL = queryParams.get(QueryParams.ruleType);
|
||||
const alertTypeFromURL = queryParams.get(QueryParams.alertType);
|
||||
@@ -23,6 +36,15 @@ function CreateRules(): JSX.Element {
|
||||
const showClassicCreateAlertsPageFlag =
|
||||
queryParams.get(QueryParams.showClassicCreateAlertsPage) === 'true';
|
||||
|
||||
const isTypeSelectionMode =
|
||||
!alertTypeFromURL && !ruleTypeFromURL && !compositeQuery;
|
||||
|
||||
useEffect(() => {
|
||||
if (isTypeSelectionMode) {
|
||||
logEvent('Alert: New alert data source selection page visited', {});
|
||||
}
|
||||
}, [isTypeSelectionMode]);
|
||||
|
||||
const alertType = useMemo(() => {
|
||||
if (ruleTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
|
||||
return AlertTypes.ANOMALY_BASED_ALERT;
|
||||
@@ -45,22 +67,142 @@ function CreateRules(): JSX.Element {
|
||||
[alertType, version],
|
||||
);
|
||||
|
||||
// Load old alerts flow always for anomaly based alerts and when showClassicCreateAlertsPage is true
|
||||
if (
|
||||
showClassicCreateAlertsPageFlag ||
|
||||
alertType === AlertTypes.ANOMALY_BASED_ALERT
|
||||
) {
|
||||
return (
|
||||
<FormAlertRules
|
||||
alertType={alertType}
|
||||
formInstance={formInstance}
|
||||
initialValue={initialAlertValue}
|
||||
ruleId=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
const handleTabChange = useCallback(
|
||||
(tab: string): void => {
|
||||
queryParams.set('tab', tab);
|
||||
queryParams.delete('subTab');
|
||||
queryParams.delete('search');
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${queryParams.toString()}`);
|
||||
},
|
||||
[safeNavigate, queryParams],
|
||||
);
|
||||
|
||||
return <CreateAlertV2 alertType={alertType} />;
|
||||
const handleSelectType = useCallback(
|
||||
(type: AlertTypes, newTab?: boolean): void => {
|
||||
if (type === AlertTypes.ANOMALY_BASED_ALERT) {
|
||||
queryParams.set(
|
||||
QueryParams.ruleType,
|
||||
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||
);
|
||||
queryParams.set(QueryParams.alertType, AlertTypes.METRICS_BASED_ALERT);
|
||||
} else {
|
||||
queryParams.set(QueryParams.ruleType, AlertDetectionTypes.THRESHOLD_ALERT);
|
||||
queryParams.set(QueryParams.alertType, type);
|
||||
}
|
||||
|
||||
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`, { newTab });
|
||||
},
|
||||
[queryParams, safeNavigate],
|
||||
);
|
||||
|
||||
const alertContent = useMemo(() => {
|
||||
if (isTypeSelectionMode) {
|
||||
return <SelectAlertType onSelect={handleSelectType} />;
|
||||
}
|
||||
|
||||
if (
|
||||
showClassicCreateAlertsPageFlag ||
|
||||
alertType === AlertTypes.ANOMALY_BASED_ALERT
|
||||
) {
|
||||
return (
|
||||
<FormAlertRules
|
||||
alertType={alertType}
|
||||
formInstance={formInstance}
|
||||
initialValue={initialAlertValue}
|
||||
ruleId=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <CreateAlertV2 alertType={alertType} />;
|
||||
}, [
|
||||
isTypeSelectionMode,
|
||||
handleSelectType,
|
||||
showClassicCreateAlertsPageFlag,
|
||||
alertType,
|
||||
formInstance,
|
||||
initialAlertValue,
|
||||
]);
|
||||
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
label: (
|
||||
<div className="periscope-tab top-level-tab">
|
||||
<GalleryVerticalEnd size={14} />
|
||||
Triggered Alerts
|
||||
</div>
|
||||
),
|
||||
key: AlertListTabs.TRIGGERED_ALERTS,
|
||||
children: null,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="periscope-tab top-level-tab">
|
||||
<Pyramid size={14} />
|
||||
Alert Rules
|
||||
</div>
|
||||
),
|
||||
key: AlertListTabs.ALERT_RULES,
|
||||
children: (
|
||||
<div className="create-alert-wrapper">
|
||||
<AlertBreadcrumb
|
||||
className="create-alert__breadcrumb"
|
||||
items={
|
||||
isTypeSelectionMode
|
||||
? [
|
||||
{
|
||||
title: 'Alert Rules',
|
||||
route: `${ROUTES.LIST_ALL_ALERT}?tab=${AlertListTabs.ALERT_RULES}`,
|
||||
},
|
||||
{ title: 'Select Alert Type', isLast: true },
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: 'Alert Rules',
|
||||
route: `${ROUTES.LIST_ALL_ALERT}?tab=${AlertListTabs.ALERT_RULES}`,
|
||||
},
|
||||
{ title: 'Select Alert Type', route: ROUTES.ALERTS_NEW },
|
||||
{
|
||||
title: ALERT_TYPE_BREADCRUMB_TITLE[alertType],
|
||||
isLast: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
{alertContent}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="periscope-tab top-level-tab">
|
||||
<ConfigureIcon width={14} height={14} />
|
||||
Configuration
|
||||
</div>
|
||||
),
|
||||
key: AlertListTabs.CONFIGURATION,
|
||||
children: null,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
destroyInactiveTabPane
|
||||
items={items}
|
||||
activeKey={AlertListTabs.ALERT_RULES}
|
||||
onChange={handleTabChange}
|
||||
className="alerts-container create-alert-tabs"
|
||||
tabBarExtraContent={
|
||||
<div className="create-alert-tabs__extra">
|
||||
<DateTimeSelector showAutoRefresh />
|
||||
<HeaderRightSection
|
||||
enableAnnouncements={false}
|
||||
enableShare
|
||||
enableFeedback
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateRules;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { RotateCcw } from '@signozhq/icons';
|
||||
import { useAlertRuleOptional } from 'providers/Alert';
|
||||
import { Labels } from 'types/api/alerts/def';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
@@ -18,6 +19,7 @@ import './styles.scss';
|
||||
|
||||
function CreateAlertHeader(): JSX.Element {
|
||||
const { alertState, setAlertState, isEditMode } = useCreateAlertState();
|
||||
const alertRuleContext = useAlertRuleOptional();
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
@@ -74,9 +76,13 @@ function CreateAlertHeader(): JSX.Element {
|
||||
<Input
|
||||
type="text"
|
||||
value={alertState.name}
|
||||
onChange={(e): void =>
|
||||
setAlertState({ type: 'SET_ALERT_NAME', payload: e.target.value })
|
||||
}
|
||||
onChange={(e): void => {
|
||||
const newName = e.target.value;
|
||||
setAlertState({ type: 'SET_ALERT_NAME', payload: newName });
|
||||
if (isEditMode && alertRuleContext?.setAlertRuleName) {
|
||||
alertRuleContext.setAlertRuleName(newName);
|
||||
}
|
||||
}}
|
||||
className="alert-header__input title"
|
||||
placeholder="Enter alert rule name"
|
||||
data-testid="alert-name-input"
|
||||
|
||||
@@ -20,6 +20,11 @@ import {
|
||||
} from './utils';
|
||||
|
||||
import './styles.scss';
|
||||
import {
|
||||
invalidateGetRuleByID,
|
||||
invalidateListRules,
|
||||
} from 'api/generated/services/rules';
|
||||
import { useQueryClient } from 'react-query';
|
||||
|
||||
function Footer(): JSX.Element {
|
||||
const {
|
||||
@@ -115,6 +120,7 @@ function Footer(): JSX.Element {
|
||||
testAlertRule,
|
||||
]);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const handleSaveAlert = useCallback((): void => {
|
||||
const payload = buildCreateThresholdAlertRulePayload({
|
||||
alertType,
|
||||
@@ -133,6 +139,9 @@ function Footer(): JSX.Element {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
void invalidateGetRuleByID(queryClient, { id: ruleId });
|
||||
void invalidateListRules(queryClient);
|
||||
|
||||
toast.success('Alert rule updated successfully');
|
||||
safeNavigate('/alerts');
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationS
|
||||
|
||||
import * as createAlertState from '../../context';
|
||||
import Footer from '../Footer';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
|
||||
// Mock the hooks used by Footer component
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
@@ -64,6 +65,12 @@ const mockAlertContextState = createMockAlertContextState({
|
||||
},
|
||||
});
|
||||
|
||||
const WrappedFooter = (): JSX.Element => (
|
||||
<MockQueryClientProvider>
|
||||
<Footer />
|
||||
</MockQueryClientProvider>
|
||||
);
|
||||
|
||||
jest
|
||||
.spyOn(createAlertState, 'useCreateAlertState')
|
||||
.mockReturnValue(mockAlertContextState);
|
||||
@@ -97,20 +104,20 @@ describe('Footer', () => {
|
||||
});
|
||||
|
||||
it('should render the component with 3 buttons', () => {
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
expect(screen.getByText(SAVE_ALERT_RULE_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(TEST_NOTIFICATION_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(DISCARD_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('discard action works correctly', () => {
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
fireEvent.click(screen.getByText(DISCARD_TEXT));
|
||||
expect(mockDiscardAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('save alert rule action works correctly', () => {
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
fireEvent.click(screen.getByText(SAVE_ALERT_RULE_TEXT));
|
||||
expect(mockCreateAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
@@ -120,13 +127,13 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isEditMode: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
fireEvent.click(screen.getByText(SAVE_ALERT_RULE_TEXT));
|
||||
expect(mockUpdateAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('test notification action works correctly', () => {
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
fireEvent.click(screen.getByText(TEST_NOTIFICATION_TEXT));
|
||||
expect(mockTestAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
@@ -136,7 +143,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isCreatingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
@@ -152,7 +159,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isUpdatingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
// Target the button elements directly instead of the text spans inside them
|
||||
expect(
|
||||
@@ -169,7 +176,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isTestingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
// Target the button elements directly instead of the text spans inside them
|
||||
expect(
|
||||
@@ -189,7 +196,7 @@ describe('Footer', () => {
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
@@ -217,7 +224,7 @@ describe('Footer', () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
@@ -245,7 +252,7 @@ describe('Footer', () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
@@ -261,7 +268,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isTestingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
// When testing alert rule, the play icon is replaced with a loader icon
|
||||
expect(
|
||||
@@ -276,7 +283,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isUpdatingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
// When updating alert rule, the check icon is replaced with a loader icon
|
||||
expect(
|
||||
@@ -291,7 +298,7 @@ describe('Footer', () => {
|
||||
...mockAlertContextState,
|
||||
isCreatingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
render(<WrappedFooter />);
|
||||
|
||||
// When creating alert rule, the check icon is replaced with a loader icon
|
||||
expect(
|
||||
|
||||
@@ -186,77 +186,40 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.section-1 {
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
padding: 16px 18px 18px 14px;
|
||||
height: unset;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 12px;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
border-top: none;
|
||||
|
||||
.ant-btn-icon {
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section-1,
|
||||
.section-2 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
padding: 16px 18px 18px 14px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
|
||||
.ant-btn-icon {
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.delete-dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
|
||||
.ant-typography {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
padding: 16px 18px 18px 14px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--bg-cherry-400) !important;
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
}
|
||||
.delete-dashboard .ant-btn {
|
||||
color: var(--bg-cherry-400) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +211,12 @@
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
|
||||
.typography-variables {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.default-value-description {
|
||||
display: block;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
SyncTooltipFilterMode,
|
||||
} from 'lib/uPlotV2/plugins/TooltipPlugin/types';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { Check, ExternalLink, Info, X } from '@signozhq/icons';
|
||||
import { Check, ExternalLink, SolidInfoCircle, X } from '@signozhq/icons';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
|
||||
import styles from './GeneralSettings.module.scss';
|
||||
@@ -201,7 +201,7 @@ function GeneralDashboardSettings(): JSX.Element {
|
||||
placement="top"
|
||||
mouseEnterDelay={0.5}
|
||||
>
|
||||
<Info size={14} className={styles.crossPanelSyncInfoIcon} />
|
||||
<SolidInfoCircle size="md" className={styles.crossPanelSyncInfoIcon} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className={styles.crossPanelSyncRow}>
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useQueryClient } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { BellDot, CircleAlert, ExternalLink, Save } from '@signozhq/icons';
|
||||
import { Button, FormInstance, Modal, SelectProps } from 'antd';
|
||||
import { Button, FormInstance, SelectProps } from 'antd';
|
||||
import { ConfirmDialog } from '@signozhq/ui/dialog';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
@@ -37,6 +38,7 @@ import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/map
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { isEmpty, isEqual } from 'lodash-es';
|
||||
import Tabs2 from 'periscope/components/Tabs2';
|
||||
import { useAlertRuleOptional } from 'providers/Alert';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -91,7 +93,6 @@ const ALERT_SETUP_GUIDE_URLS: Record<AlertTypes, string> = {
|
||||
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function FormAlertRules({
|
||||
alertType,
|
||||
formInstance,
|
||||
@@ -159,9 +160,36 @@ function FormAlertRules({
|
||||
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
|
||||
const [yAxisUnit, setYAxisUnit] = useState<string>(currentQuery.unit || '');
|
||||
|
||||
const alertRuleContext = useAlertRuleOptional();
|
||||
const providerAlertName = alertRuleContext?.alertRuleName;
|
||||
useEffect(() => {
|
||||
if (providerAlertName) {
|
||||
setAlertDef((prev) => {
|
||||
if (prev.alert === providerAlertName) {
|
||||
return prev;
|
||||
}
|
||||
return { ...prev, alert: providerAlertName };
|
||||
});
|
||||
formInstance.setFieldsValue({ alert: providerAlertName });
|
||||
}
|
||||
}, [providerAlertName, formInstance]);
|
||||
|
||||
// Wrap setAlertDef to sync alert name to provider when user types
|
||||
const handleSetAlertDef = useCallback(
|
||||
(newDef: AlertDef) => {
|
||||
setAlertDef(newDef);
|
||||
// Sync alert name change to provider for header display
|
||||
if (newDef.alert !== alertDef.alert && alertRuleContext?.setAlertRuleName) {
|
||||
alertRuleContext.setAlertRuleName(newDef.alert);
|
||||
}
|
||||
},
|
||||
[alertDef.alert, alertRuleContext],
|
||||
);
|
||||
|
||||
const alertTypeFromURL = urlQuery.get(QueryParams.ruleType);
|
||||
|
||||
const [detectionMethod, setDetectionMethod] = useState<string | null>(null);
|
||||
const [isConfirmSaveOpen, setIsConfirmSaveOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEqual(currentQuery.unit, yAxisUnit)) {
|
||||
@@ -577,19 +605,16 @@ function FormAlertRules({
|
||||
});
|
||||
|
||||
// invalidate rule in cache
|
||||
ruleCache.invalidateQueries([
|
||||
await ruleCache.invalidateQueries([
|
||||
REACT_QUERY_KEY.ALERT_RULE_DETAILS,
|
||||
`${ruleId}`,
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setTimeout(() => {
|
||||
urlQuery.delete(QueryParams.compositeQuery);
|
||||
urlQuery.delete(QueryParams.panelTypes);
|
||||
urlQuery.delete(QueryParams.ruleId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
|
||||
}, 2000);
|
||||
urlQuery.delete(QueryParams.compositeQuery);
|
||||
urlQuery.delete(QueryParams.panelTypes);
|
||||
urlQuery.delete(QueryParams.ruleId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
|
||||
} catch (e) {
|
||||
const apiError = convertToApiError(e as AxiosError<RenderErrorResponseDTO>);
|
||||
logData = {
|
||||
@@ -625,24 +650,9 @@ function FormAlertRules({
|
||||
urlQuery,
|
||||
]);
|
||||
|
||||
const onSaveHandler = useCallback(async () => {
|
||||
const content = (
|
||||
<Typography.Text>
|
||||
{' '}
|
||||
{t('confirm_save_content_part1')}{' '}
|
||||
<QueryTypeTag queryType={currentQuery.queryType} />{' '}
|
||||
{t('confirm_save_content_part2')}
|
||||
</Typography.Text>
|
||||
);
|
||||
Modal.confirm({
|
||||
icon: <CircleAlert size="md" />,
|
||||
title: t('confirm_save_title'),
|
||||
centered: true,
|
||||
content,
|
||||
onOk: saveRule,
|
||||
className: 'create-alert-modal',
|
||||
});
|
||||
}, [t, saveRule, currentQuery]);
|
||||
const onSaveHandler = useCallback(() => {
|
||||
setIsConfirmSaveOpen(true);
|
||||
}, []);
|
||||
|
||||
const onTestRuleHandler = useCallback(async () => {
|
||||
if (!isFormValid()) {
|
||||
@@ -696,7 +706,7 @@ function FormAlertRules({
|
||||
const renderBasicInfo = (): JSX.Element => (
|
||||
<BasicInfo
|
||||
alertDef={alertDef}
|
||||
setAlertDef={setAlertDef}
|
||||
setAlertDef={handleSetAlertDef}
|
||||
isNewRule={isNewRule}
|
||||
/>
|
||||
);
|
||||
@@ -988,6 +998,27 @@ function FormAlertRules({
|
||||
</ButtonContainer>
|
||||
</MainFormContainer>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={isConfirmSaveOpen}
|
||||
onOpenChange={setIsConfirmSaveOpen}
|
||||
title={t('confirm_save_title')}
|
||||
titleIcon={<CircleAlert size={14} />}
|
||||
confirmText="OK"
|
||||
confirmColor="primary"
|
||||
onConfirm={async (): Promise<boolean> => {
|
||||
await saveRule();
|
||||
return true;
|
||||
}}
|
||||
onCancel={() => setIsConfirmSaveOpen(false)}
|
||||
width="narrow"
|
||||
>
|
||||
<Typography.Text>
|
||||
{t('confirm_save_content_part1')}{' '}
|
||||
<QueryTypeTag queryType={currentQuery.queryType} />{' '}
|
||||
{t('confirm_save_content_part2')}
|
||||
</Typography.Text>
|
||||
</ConfirmDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,10 +26,13 @@
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.widget-header-title {
|
||||
max-width: 80%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.widget-header-actions {
|
||||
|
||||
@@ -438,9 +438,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
data: {
|
||||
name: values.name,
|
||||
tags: updatedTags,
|
||||
expires_at: new Date(
|
||||
dayjs(values.expires_at).endOf('day').toISOString(),
|
||||
),
|
||||
expires_at: dayjs(values.expires_at).endOf('day').toISOString(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -471,13 +469,11 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
const requestPayload = {
|
||||
name: values.name,
|
||||
tags: updatedTags,
|
||||
expires_at: new Date(dayjs(values.expires_at).endOf('day').toISOString()),
|
||||
expires_at: dayjs(values.expires_at).endOf('day').toISOString(),
|
||||
};
|
||||
|
||||
createIngestionKey(
|
||||
{
|
||||
data: requestPayload,
|
||||
},
|
||||
{ data: requestPayload },
|
||||
{
|
||||
onSuccess: (_data) => {
|
||||
notifications.success({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user