mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-09 02:20:26 +01:00
Compare commits
436 Commits
issue_5222
...
nv/dashboa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
852ce0cfce | ||
|
|
62c0c52bec | ||
|
|
36dd325e9a | ||
|
|
c51ef198fe | ||
|
|
1b4f1c7161 | ||
|
|
f82dd1b96a | ||
|
|
f0deed6e0f | ||
|
|
e8791fe1ed | ||
|
|
4fe9222a5c | ||
|
|
7d9d4b923d | ||
|
|
67ed248fef | ||
|
|
77cb60c106 | ||
|
|
1fbe05f450 | ||
|
|
43d588a870 | ||
|
|
6309e27168 | ||
|
|
564e79137e | ||
|
|
92ef8b73a9 | ||
|
|
98df6ead80 | ||
|
|
7c7329cfd7 | ||
|
|
00a99999ac | ||
|
|
fd1a94ca41 | ||
|
|
29e8f651f5 | ||
|
|
3809dc1d31 | ||
|
|
9a5a24392f | ||
|
|
de42bc9f73 | ||
|
|
0ddadb051d | ||
|
|
9e8da30c3b | ||
|
|
97de2551eb | ||
|
|
67affa0fd5 | ||
|
|
226d924489 | ||
|
|
643770d790 | ||
|
|
f92473230a | ||
|
|
93452ebde9 | ||
|
|
d9b316fc0a | ||
|
|
c5c86c1bbf | ||
|
|
19f68b1ac1 | ||
|
|
932080a83c | ||
|
|
9135664188 | ||
|
|
2d061f3a70 | ||
|
|
a08bd8d126 | ||
|
|
6a3d1b54eb | ||
|
|
4c10fb0df1 | ||
|
|
deea23d441 | ||
|
|
3b2586f386 | ||
|
|
cd44095dc7 | ||
|
|
fb2834f812 | ||
|
|
66534bc7c6 | ||
|
|
e8214739b8 | ||
|
|
0a2fe1c8f6 | ||
|
|
d40d2ddf60 | ||
|
|
879258ec28 | ||
|
|
a36c7fbde1 | ||
|
|
005718cc3b | ||
|
|
bc0623d07a | ||
|
|
7019df2470 | ||
|
|
faf877de65 | ||
|
|
5e5656881d | ||
|
|
5d60c34761 | ||
|
|
9bf42aa1da | ||
|
|
75b422d236 | ||
|
|
a6e3a6efa2 | ||
|
|
158a2eac8b | ||
|
|
c0e67a8ca3 | ||
|
|
9b7c820f87 | ||
|
|
43ff0dc90f | ||
|
|
c8cbac7725 | ||
|
|
5efa8d226b | ||
|
|
cc1feb43d0 | ||
|
|
13d9641f9d | ||
|
|
9f559d33d7 | ||
|
|
f56f7381a5 | ||
|
|
3656d117cc | ||
|
|
8e39f44a9e | ||
|
|
e92a0e953c | ||
|
|
7add0b924c | ||
|
|
d2070d4844 | ||
|
|
e9b0c32f19 | ||
|
|
ae18dbc722 | ||
|
|
6771e06d15 | ||
|
|
61b8a41c3d | ||
|
|
9789a5be89 | ||
|
|
09bf5267d6 | ||
|
|
87ff61e887 | ||
|
|
3c14d0e53c | ||
|
|
f57fc261b9 | ||
|
|
8afb247046 | ||
|
|
3ee706c68e | ||
|
|
58767e67c0 | ||
|
|
ccecc13bb9 | ||
|
|
1c8b2c1d21 | ||
|
|
8fbec89aab | ||
|
|
40da02e5b4 | ||
|
|
6704e15a01 | ||
|
|
d05697fdba | ||
|
|
fd3ddab9fd | ||
|
|
1b3b891d5c | ||
|
|
d903f6cb1b | ||
|
|
fbf30cc192 | ||
|
|
ecd9670bf6 | ||
|
|
b5fb45904c | ||
|
|
c24caf0e9a | ||
|
|
dc42fae378 | ||
|
|
94e70179d7 | ||
|
|
063c104b80 | ||
|
|
937ebfd843 | ||
|
|
d8da99fcbb | ||
|
|
075e8d3029 | ||
|
|
f7fc3eade8 | ||
|
|
9415c06166 | ||
|
|
fa8139b279 | ||
|
|
3271e35eeb | ||
|
|
a48beef8ec | ||
|
|
ad4e3dcf45 | ||
|
|
2e8295f0b3 | ||
|
|
3e6394ba50 | ||
|
|
95da13ecb9 | ||
|
|
8b38e3969f | ||
|
|
f3d097a61a | ||
|
|
81249342ff | ||
|
|
5ce097ed2a | ||
|
|
ad9b17d9ae | ||
|
|
6d5eed3782 | ||
|
|
3c985af24c | ||
|
|
c3f50b5db8 | ||
|
|
000dcc10b2 | ||
|
|
d7ffbe3f9f | ||
|
|
72cbc0450b | ||
|
|
7d7fd425e2 | ||
|
|
e3e2ea61a0 | ||
|
|
a870a98a80 | ||
|
|
d55846af03 | ||
|
|
55cfe57a9e | ||
|
|
0a4c75d899 | ||
|
|
44292bdc1d | ||
|
|
5e6b71e56e | ||
|
|
abeab11271 | ||
|
|
26b007cce5 | ||
|
|
0d1766b7c2 | ||
|
|
61c15c704b | ||
|
|
6702930688 | ||
|
|
4a24ee44e8 | ||
|
|
0045ecadab | ||
|
|
69aad53eb4 | ||
|
|
7bacb03483 | ||
|
|
b610056954 | ||
|
|
db77b398e7 | ||
|
|
574867bafb | ||
|
|
d87edca9d1 | ||
|
|
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 | ||
|
|
4374f75394 | ||
|
|
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 |
3
.github/workflows/integrationci.yaml
vendored
3
.github/workflows/integrationci.yaml
vendored
@@ -39,7 +39,6 @@ jobs:
|
||||
matrix:
|
||||
suite:
|
||||
- alerts
|
||||
- basepath
|
||||
- callbackauthn
|
||||
- cloudintegrations
|
||||
- dashboard
|
||||
@@ -84,7 +83,7 @@ jobs:
|
||||
run: |
|
||||
cd tests && uv sync
|
||||
- name: webdriver
|
||||
if: matrix.suite == 'callbackauthn' || matrix.suite == 'basepath'
|
||||
if: matrix.suite == 'callbackauthn'
|
||||
run: |
|
||||
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
|
||||
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee -a /etc/apt/sources.list.d/google-chrome.list
|
||||
|
||||
@@ -91,7 +91,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
sqlstoreProviderFactories(),
|
||||
signoz.NewTelemetryStoreProviderFactories(),
|
||||
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
|
||||
return signoz.NewAuthNs(ctx, providerSettings, store, licensing, config.Global)
|
||||
return signoz.NewAuthNs(ctx, providerSettings, store, licensing)
|
||||
},
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore, config authz.Config, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
|
||||
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore, config)
|
||||
|
||||
@@ -107,17 +107,17 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
sqlstoreProviderFactories(),
|
||||
signoz.NewTelemetryStoreProviderFactories(),
|
||||
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
|
||||
samlCallbackAuthN, err := samlcallbackauthn.New(ctx, store, licensing, config.Global)
|
||||
samlCallbackAuthN, err := samlcallbackauthn.New(ctx, store, licensing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oidcCallbackAuthN, err := oidccallbackauthn.New(store, licensing, providerSettings, config.Global)
|
||||
oidcCallbackAuthN, err := oidccallbackauthn.New(store, licensing, providerSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authNs, err := signoz.NewAuthNs(ctx, providerSettings, store, licensing, config.Global)
|
||||
authNs, err := signoz.NewAuthNs(ctx, providerSettings, store, licensing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -440,17 +440,6 @@ traces:
|
||||
max_depth_to_auto_expand: 5
|
||||
# Threshold below which all spans are returned without windowing.
|
||||
max_limit_to_select_all_spans: 10000
|
||||
flamegraph:
|
||||
# Maximum number of BFS depth levels included in a windowed response.
|
||||
max_selected_levels: 50
|
||||
# Maximum spans per level before sampling is applied.
|
||||
max_spans_per_level: 100
|
||||
# Number of highest-latency spans always included when sampling a level.
|
||||
sampling_top_latency_count: 5
|
||||
# Number of timestamp buckets used for uniform sampling within a level.
|
||||
sampling_bucket_count: 50
|
||||
# Threshold below which all spans are returned without windowing or sampling.
|
||||
select_all_spans_limit: 100000
|
||||
|
||||
##################### Authz #################################
|
||||
authz:
|
||||
|
||||
@@ -1360,7 +1360,6 @@ components:
|
||||
- sqs
|
||||
- storageaccountsblob
|
||||
- cdnprofile
|
||||
- aks
|
||||
type: string
|
||||
CloudintegrationtypesServiceMetadata:
|
||||
properties:
|
||||
@@ -2584,6 +2583,41 @@ components:
|
||||
$ref: '#/components/schemas/DashboardtypesVariable'
|
||||
type: array
|
||||
type: object
|
||||
DashboardtypesDashboardView:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesDashboardViewData'
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
orgId:
|
||||
type: string
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- data
|
||||
- orgId
|
||||
type: object
|
||||
DashboardtypesDashboardViewData:
|
||||
properties:
|
||||
order:
|
||||
$ref: '#/components/schemas/DashboardtypesListOrder'
|
||||
query:
|
||||
type: string
|
||||
sort:
|
||||
$ref: '#/components/schemas/DashboardtypesListSort'
|
||||
version:
|
||||
type: string
|
||||
required:
|
||||
- version
|
||||
type: object
|
||||
DashboardtypesDatasourcePlugin:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/DashboardtypesDatasourcePluginVariantStruct'
|
||||
@@ -2653,7 +2687,7 @@ components:
|
||||
$ref: '#/components/schemas/DashboardtypesDashboardSpec'
|
||||
tags:
|
||||
items:
|
||||
$ref: '#/components/schemas/TagtypesPostableTag'
|
||||
$ref: '#/components/schemas/TagtypesGettableTag'
|
||||
nullable: true
|
||||
type: array
|
||||
updatedAt:
|
||||
@@ -2771,6 +2805,11 @@ components:
|
||||
- solid
|
||||
- dashed
|
||||
type: string
|
||||
DashboardtypesListOrder:
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
type: string
|
||||
DashboardtypesListPanelSpec:
|
||||
properties:
|
||||
selectFields:
|
||||
@@ -2778,6 +2817,12 @@ components:
|
||||
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
|
||||
type: array
|
||||
type: object
|
||||
DashboardtypesListSort:
|
||||
enum:
|
||||
- updated_at
|
||||
- created_at
|
||||
- name
|
||||
type: string
|
||||
DashboardtypesListVariableSpec:
|
||||
properties:
|
||||
allowAllValue:
|
||||
@@ -2800,6 +2845,83 @@ components:
|
||||
nullable: true
|
||||
type: string
|
||||
type: object
|
||||
DashboardtypesListableDashboardV2:
|
||||
properties:
|
||||
dashboards:
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardtypesListedDashboardV2'
|
||||
type: array
|
||||
tags:
|
||||
items:
|
||||
$ref: '#/components/schemas/TagtypesGettableTag'
|
||||
type: array
|
||||
total:
|
||||
format: int64
|
||||
type: integer
|
||||
required:
|
||||
- dashboards
|
||||
- total
|
||||
- tags
|
||||
type: object
|
||||
DashboardtypesListableDashboardView:
|
||||
properties:
|
||||
views:
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardtypesDashboardView'
|
||||
type: array
|
||||
required:
|
||||
- views
|
||||
type: object
|
||||
DashboardtypesListedDashboardV2:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
image:
|
||||
type: string
|
||||
locked:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
orgId:
|
||||
type: string
|
||||
pinned:
|
||||
type: boolean
|
||||
schemaVersion:
|
||||
type: string
|
||||
source:
|
||||
$ref: '#/components/schemas/DashboardtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/DashboardtypesListedDashboardV2Spec'
|
||||
tags:
|
||||
items:
|
||||
$ref: '#/components/schemas/TagtypesGettableTag'
|
||||
type: array
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- orgId
|
||||
- locked
|
||||
- source
|
||||
- schemaVersion
|
||||
- name
|
||||
- pinned
|
||||
- tags
|
||||
- spec
|
||||
type: object
|
||||
DashboardtypesListedDashboardV2Spec:
|
||||
properties:
|
||||
display:
|
||||
$ref: '#/components/schemas/CommonDisplay'
|
||||
type: object
|
||||
DashboardtypesNumberPanelSpec:
|
||||
properties:
|
||||
formatting:
|
||||
@@ -2993,6 +3115,16 @@ components:
|
||||
- tags
|
||||
- spec
|
||||
type: object
|
||||
DashboardtypesPostableDashboardView:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesDashboardViewData'
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- data
|
||||
type: object
|
||||
DashboardtypesPostablePublicDashboard:
|
||||
properties:
|
||||
defaultTimeRange:
|
||||
@@ -6639,70 +6771,6 @@ components:
|
||||
- attribute
|
||||
- resource
|
||||
type: string
|
||||
SpantypesFlamegraphSpan:
|
||||
properties:
|
||||
attributes:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
durationNano:
|
||||
minimum: 0
|
||||
type: integer
|
||||
event:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesEvent'
|
||||
type: array
|
||||
hasError:
|
||||
type: boolean
|
||||
level:
|
||||
format: int64
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
parentSpanId:
|
||||
type: string
|
||||
resource:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
spanId:
|
||||
type: string
|
||||
timestamp:
|
||||
minimum: 0
|
||||
type: integer
|
||||
required:
|
||||
- spanId
|
||||
- parentSpanId
|
||||
- timestamp
|
||||
- durationNano
|
||||
- hasError
|
||||
- name
|
||||
- level
|
||||
- event
|
||||
- attributes
|
||||
- resource
|
||||
type: object
|
||||
SpantypesGettableFlamegraphTrace:
|
||||
properties:
|
||||
endTimestampMillis:
|
||||
format: int64
|
||||
type: integer
|
||||
hasMore:
|
||||
type: boolean
|
||||
spans:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesFlamegraphSpan'
|
||||
type: array
|
||||
type: array
|
||||
startTimestampMillis:
|
||||
format: int64
|
||||
type: integer
|
||||
required:
|
||||
- spans
|
||||
- startTimestampMillis
|
||||
- endTimestampMillis
|
||||
- hasMore
|
||||
type: object
|
||||
SpantypesGettableSpanMapperGroups:
|
||||
properties:
|
||||
items:
|
||||
@@ -6768,15 +6836,6 @@ components:
|
||||
traceId:
|
||||
type: string
|
||||
type: object
|
||||
SpantypesPostableFlamegraph:
|
||||
properties:
|
||||
selectFields:
|
||||
items:
|
||||
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
|
||||
type: array
|
||||
selectedSpanId:
|
||||
type: string
|
||||
type: object
|
||||
SpantypesPostableSpanMapper:
|
||||
properties:
|
||||
config:
|
||||
@@ -7072,6 +7131,16 @@ components:
|
||||
required:
|
||||
- references
|
||||
type: object
|
||||
TagtypesGettableTag:
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
required:
|
||||
- key
|
||||
- value
|
||||
type: object
|
||||
TagtypesPostableTag:
|
||||
properties:
|
||||
key:
|
||||
@@ -13107,6 +13176,80 @@ paths:
|
||||
tags:
|
||||
- preferences
|
||||
/api/v2/dashboards:
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns a page of v2-shape dashboards for the calling user's org.
|
||||
Supports a filter DSL (`query`), sort (`updated_at`/`created_at`/`name`),
|
||||
order (`asc`/`desc`), and offset-based pagination (`limit`/`offset`).
|
||||
operationId: ListDashboardsV2
|
||||
parameters:
|
||||
- in: query
|
||||
name: query
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: sort
|
||||
schema:
|
||||
$ref: '#/components/schemas/DashboardtypesListSort'
|
||||
- in: query
|
||||
name: order
|
||||
schema:
|
||||
$ref: '#/components/schemas/DashboardtypesListOrder'
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: offset
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesListableDashboardV2'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: List dashboards (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint creates a dashboard in the v2 format that follows
|
||||
@@ -13165,6 +13308,62 @@ paths:
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/dashboards/{id}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: This endpoint deletes a v2-shape dashboard along with its tag relations.
|
||||
Locked dashboards are rejected.
|
||||
operationId: DeleteDashboardV2
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Delete dashboard (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns a v2-shape dashboard.
|
||||
@@ -13484,6 +13683,351 @@ paths:
|
||||
summary: Lock dashboard (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/dashboards/{id}/pins/me:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: Removes the pin for the calling user. Idempotent — unpinning a
|
||||
dashboard that wasn't pinned still returns 204.
|
||||
operationId: UnpinDashboardV2
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Unpin a dashboard for the current user (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
put:
|
||||
deprecated: false
|
||||
description: 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.
|
||||
operationId: PinDashboardV2
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Pin a dashboard for the current user (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/dashboards/views:
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns every saved view in the calling user's org. Saved views
|
||||
are shared org-wide; any user may read, create, edit, and delete any view.
|
||||
operationId: ListDashboardViews
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesListableDashboardView'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: List dashboard saved views
|
||||
tags:
|
||||
- dashboard
|
||||
post:
|
||||
deprecated: false
|
||||
description: Persists the calling user's dashboard listing state (query, sort,
|
||||
order) as a named, reusable view shared across the org.
|
||||
operationId: CreateDashboardView
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DashboardtypesPostableDashboardView'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesDashboardView'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: Created
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Create dashboard saved view
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/dashboards/views/{id}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: Removes a saved view. Saved views are shared org-wide; any user
|
||||
in the org may delete any view. Idempotent — deleting a non-existent view
|
||||
returns 404.
|
||||
operationId: DeleteDashboardView
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Delete dashboard saved view
|
||||
tags:
|
||||
- dashboard
|
||||
put:
|
||||
deprecated: false
|
||||
description: Replaces a saved view's name and data. Saved views are shared org-wide;
|
||||
any user in the org may edit any view.
|
||||
operationId: UpdateDashboardView
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DashboardtypesPostableDashboardView'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesDashboardView'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Update dashboard saved view
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/factor_password/forgot:
|
||||
post:
|
||||
deprecated: false
|
||||
@@ -20609,75 +21153,6 @@ paths:
|
||||
summary: Put profile in Zeus for a deployment.
|
||||
tags:
|
||||
- zeus
|
||||
/api/v3/traces/{traceID}/flamegraph:
|
||||
post:
|
||||
deprecated: false
|
||||
description: Returns the flamegraph view of spans for a given trace ID.
|
||||
operationId: GetFlamegraph
|
||||
parameters:
|
||||
- in: path
|
||||
name: traceID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpantypesPostableFlamegraph'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SpantypesGettableFlamegraphTrace'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Get flamegraph view for a trace
|
||||
tags:
|
||||
- tracedetail
|
||||
/api/v3/traces/{traceID}/waterfall:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -5,12 +5,10 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/http/client"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
@@ -28,14 +26,13 @@ var defaultScopes []string = []string{"email", "profile", oidc.ScopeOpenID}
|
||||
var _ authn.CallbackAuthN = (*AuthN)(nil)
|
||||
|
||||
type AuthN struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
store authtypes.AuthNStore
|
||||
licensing licensing.Licensing
|
||||
httpClient *client.Client
|
||||
globalConfig global.Config
|
||||
settings factory.ScopedProviderSettings
|
||||
store authtypes.AuthNStore
|
||||
licensing licensing.Licensing
|
||||
httpClient *client.Client
|
||||
}
|
||||
|
||||
func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSettings factory.ProviderSettings, globalConfig global.Config) (*AuthN, error) {
|
||||
func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSettings factory.ProviderSettings) (*AuthN, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn")
|
||||
|
||||
httpClient, err := client.New(providerSettings.Logger, providerSettings.TracerProvider, providerSettings.MeterProvider)
|
||||
@@ -44,11 +41,10 @@ func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSett
|
||||
}
|
||||
|
||||
return &AuthN{
|
||||
settings: settings,
|
||||
store: store,
|
||||
licensing: licensing,
|
||||
httpClient: httpClient,
|
||||
globalConfig: globalConfig,
|
||||
settings: settings,
|
||||
store: store,
|
||||
licensing: licensing,
|
||||
httpClient: httpClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -201,7 +197,7 @@ func (a *AuthN) oidcProviderAndoauth2Config(ctx context.Context, siteURL *url.UR
|
||||
RedirectURL: (&url.URL{
|
||||
Scheme: siteURL.Scheme,
|
||||
Host: siteURL.Host,
|
||||
Path: path.Join(a.globalConfig.ExternalPath(), redirectPath),
|
||||
Path: redirectPath,
|
||||
}).String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -6,12 +6,10 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -26,16 +24,14 @@ const (
|
||||
var _ authn.CallbackAuthN = (*AuthN)(nil)
|
||||
|
||||
type AuthN struct {
|
||||
store authtypes.AuthNStore
|
||||
licensing licensing.Licensing
|
||||
globalConfig global.Config
|
||||
store authtypes.AuthNStore
|
||||
licensing licensing.Licensing
|
||||
}
|
||||
|
||||
func New(ctx context.Context, store authtypes.AuthNStore, licensing licensing.Licensing, globalConfig global.Config) (*AuthN, error) {
|
||||
func New(ctx context.Context, store authtypes.AuthNStore, licensing licensing.Licensing) (*AuthN, error) {
|
||||
return &AuthN{
|
||||
store: store,
|
||||
licensing: licensing,
|
||||
globalConfig: globalConfig,
|
||||
store: store,
|
||||
licensing: licensing,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -136,7 +132,7 @@ func (a *AuthN) serviceProvider(siteURL *url.URL, authDomain *authtypes.AuthDoma
|
||||
return nil, err
|
||||
}
|
||||
|
||||
acsURL := &url.URL{Scheme: siteURL.Scheme, Host: siteURL.Host, Path: path.Join(a.globalConfig.ExternalPath(), redirectPath)}
|
||||
acsURL := &url.URL{Scheme: siteURL.Scheme, Host: siteURL.Host, Path: redirectPath}
|
||||
|
||||
// Note:
|
||||
// The ServiceProviderIssuer is the client id in case of keycloak. Since we set it to the host here, we need to set the client id == host in keycloak.
|
||||
|
||||
@@ -229,10 +229,47 @@ func (module *module) PatchV2(ctx context.Context, orgID valuer.UUID, id valuer.
|
||||
return module.pkgDashboardModule.PatchV2(ctx, orgID, id, updatedBy, patch)
|
||||
}
|
||||
|
||||
func (module *module) DeleteV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
return module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := module.store.DeletePublic(ctx, id.String()); err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
return module.pkgDashboardModule.DeleteV2(ctx, orgID, id)
|
||||
})
|
||||
}
|
||||
|
||||
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) CreateView(ctx context.Context, orgID valuer.UUID, postable dashboardtypes.PostableDashboardView) (*dashboardtypes.DashboardView, error) {
|
||||
return module.pkgDashboardModule.CreateView(ctx, orgID, postable)
|
||||
}
|
||||
|
||||
func (module *module) ListViews(ctx context.Context, orgID valuer.UUID) (*dashboardtypes.ListableDashboardView, error) {
|
||||
return module.pkgDashboardModule.ListViews(ctx, orgID)
|
||||
}
|
||||
|
||||
func (module *module) UpdateView(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updateable dashboardtypes.UpdateableDashboardView) (*dashboardtypes.DashboardView, error) {
|
||||
return module.pkgDashboardModule.UpdateView(ctx, orgID, id, updateable)
|
||||
}
|
||||
|
||||
func (module *module) DeleteView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
return module.pkgDashboardModule.DeleteView(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
|
||||
return module.pkgDashboardModule.Get(ctx, orgID, id)
|
||||
}
|
||||
|
||||
@@ -16,10 +16,11 @@ func newFormatter(dialect schema.Dialect) sqlstore.SQLFormatter {
|
||||
}
|
||||
|
||||
func (f *formatter) JSONExtractString(column, path string) []byte {
|
||||
var sql []byte
|
||||
sql = f.bunf.AppendIdent(sql, column)
|
||||
sql = append(sql, f.convertJSONPathToPostgres(path)...)
|
||||
return sql
|
||||
ops := f.convertJSONPathToPostgres(path)
|
||||
if len(ops) == 0 {
|
||||
return f.bunf.AppendIdent(nil, column)
|
||||
}
|
||||
return append(f.TextToJsonColumn(column), ops...)
|
||||
}
|
||||
|
||||
func (f *formatter) JSONType(column, path string) []byte {
|
||||
|
||||
@@ -18,19 +18,19 @@ func TestJSONExtractString(t *testing.T) {
|
||||
name: "simple path",
|
||||
column: "data",
|
||||
path: "$.field",
|
||||
expected: `"data"->>'field'`,
|
||||
expected: `"data"::jsonb->>'field'`,
|
||||
},
|
||||
{
|
||||
name: "nested path",
|
||||
column: "metadata",
|
||||
path: "$.user.name",
|
||||
expected: `"metadata"->'user'->>'name'`,
|
||||
expected: `"metadata"::jsonb->'user'->>'name'`,
|
||||
},
|
||||
{
|
||||
name: "deeply nested path",
|
||||
column: "json_col",
|
||||
path: "$.level1.level2.level3",
|
||||
expected: `"json_col"->'level1'->'level2'->>'level3'`,
|
||||
expected: `"json_col"::jsonb->'level1'->'level2'->>'level3'`,
|
||||
},
|
||||
{
|
||||
name: "root path",
|
||||
|
||||
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"::jsonb->'spec'->'display'->>'name' = ?`,
|
||||
wantArgs: []any{"overview"},
|
||||
},
|
||||
{
|
||||
name: "name CONTAINS — same JSON path, LIKE pattern",
|
||||
query: `name CONTAINS 'overview'`,
|
||||
wantSQL: `"dashboard"."data"::jsonb->'spec'->'display'->>'name' LIKE ? ESCAPE '\'`,
|
||||
wantArgs: []any{"%overview%"},
|
||||
},
|
||||
{
|
||||
name: "name ILIKE — LOWER wraps the JSON path",
|
||||
query: `name ILIKE 'Prod%'`,
|
||||
wantSQL: `lower("dashboard"."data"::jsonb->'spec'->'display'->>'name') LIKE LOWER(?) ESCAPE '\'`,
|
||||
wantArgs: []any{"Prod%"},
|
||||
},
|
||||
{
|
||||
name: "description = follows the same path shape",
|
||||
query: `description = 'd1'`,
|
||||
wantSQL: `"dashboard"."data"::jsonb->'spec'->'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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,17 @@ import type {
|
||||
|
||||
import type {
|
||||
CreateDashboardV2201,
|
||||
CreateDashboardView201,
|
||||
CreatePublicDashboard201,
|
||||
CreatePublicDashboardPathParameters,
|
||||
DashboardtypesPatchableDashboardV2DTO,
|
||||
DashboardtypesPostableDashboardV2DTO,
|
||||
DashboardtypesPostableDashboardViewDTO,
|
||||
DashboardtypesPostablePublicDashboardDTO,
|
||||
DashboardtypesUpdatableDashboardV2DTO,
|
||||
DashboardtypesUpdatablePublicDashboardDTO,
|
||||
DeleteDashboardV2PathParameters,
|
||||
DeleteDashboardViewPathParameters,
|
||||
DeletePublicDashboardPathParameters,
|
||||
GetDashboardV2200,
|
||||
GetDashboardV2PathParameters,
|
||||
@@ -35,13 +39,20 @@ import type {
|
||||
GetPublicDashboardPathParameters,
|
||||
GetPublicDashboardWidgetQueryRange200,
|
||||
GetPublicDashboardWidgetQueryRangePathParameters,
|
||||
ListDashboardViews200,
|
||||
ListDashboardsV2200,
|
||||
ListDashboardsV2Params,
|
||||
LockDashboardV2PathParameters,
|
||||
PatchDashboardV2200,
|
||||
PatchDashboardV2PathParameters,
|
||||
PinDashboardV2PathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
UnlockDashboardV2PathParameters,
|
||||
UnpinDashboardV2PathParameters,
|
||||
UpdateDashboardV2200,
|
||||
UpdateDashboardV2PathParameters,
|
||||
UpdateDashboardView200,
|
||||
UpdateDashboardViewPathParameters,
|
||||
UpdatePublicDashboardPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
@@ -641,6 +652,103 @@ 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`/`name`), order (`asc`/`desc`), and offset-based pagination (`limit`/`offset`).
|
||||
* @summary List dashboards (v2)
|
||||
*/
|
||||
export const listDashboardsV2 = (
|
||||
params?: ListDashboardsV2Params,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListDashboardsV2200>({
|
||||
url: `/api/v2/dashboards`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListDashboardsV2QueryKey = (
|
||||
params?: ListDashboardsV2Params,
|
||||
) => {
|
||||
return [`/api/v2/dashboards`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getListDashboardsV2QueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listDashboardsV2>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params?: ListDashboardsV2Params,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listDashboardsV2>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListDashboardsV2QueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof listDashboardsV2>>> = ({
|
||||
signal,
|
||||
}) => listDashboardsV2(params, 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>,
|
||||
>(
|
||||
params?: ListDashboardsV2Params,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listDashboardsV2>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListDashboardsV2QueryOptions(params, 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,
|
||||
params?: ListDashboardsV2Params,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListDashboardsV2QueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint creates a dashboard in the v2 format that follows Perses spec.
|
||||
* @summary Create dashboard (v2)
|
||||
@@ -724,6 +832,85 @@ export const useCreateDashboardV2 = <
|
||||
> => {
|
||||
return useMutation(getCreateDashboardV2MutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint deletes a v2-shape dashboard along with its tag relations. Locked dashboards are rejected.
|
||||
* @summary Delete dashboard (v2)
|
||||
*/
|
||||
export const deleteDashboardV2 = (
|
||||
{ id }: DeleteDashboardV2PathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v2/dashboards/${id}`,
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteDashboardV2MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: DeleteDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: DeleteDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteDashboardV2'];
|
||||
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 deleteDashboardV2>>,
|
||||
{ pathParams: DeleteDashboardV2PathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteDashboardV2(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteDashboardV2MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteDashboardV2>>
|
||||
>;
|
||||
|
||||
export type DeleteDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Delete dashboard (v2)
|
||||
*/
|
||||
export const useDeleteDashboardV2 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: DeleteDashboardV2PathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteDashboardV2>>,
|
||||
TError,
|
||||
{ pathParams: DeleteDashboardV2PathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getDeleteDashboardV2MutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint returns a v2-shape dashboard.
|
||||
* @summary Get dashboard (v2)
|
||||
@@ -1181,3 +1368,509 @@ export const useLockDashboardV2 = <
|
||||
> => {
|
||||
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));
|
||||
};
|
||||
/**
|
||||
* Returns every saved view in the calling user's org. Saved views are shared org-wide; any user may read, create, edit, and delete any view.
|
||||
* @summary List dashboard saved views
|
||||
*/
|
||||
export const listDashboardViews = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<ListDashboardViews200>({
|
||||
url: `/api/v2/dashboards/views`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListDashboardViewsQueryKey = () => {
|
||||
return [`/api/v2/dashboards/views`] as const;
|
||||
};
|
||||
|
||||
export const getListDashboardViewsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listDashboardViews>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listDashboardViews>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListDashboardViewsQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof listDashboardViews>>
|
||||
> = ({ signal }) => listDashboardViews(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listDashboardViews>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListDashboardViewsQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listDashboardViews>>
|
||||
>;
|
||||
export type ListDashboardViewsQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List dashboard saved views
|
||||
*/
|
||||
|
||||
export function useListDashboardViews<
|
||||
TData = Awaited<ReturnType<typeof listDashboardViews>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listDashboardViews>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListDashboardViewsQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List dashboard saved views
|
||||
*/
|
||||
export const invalidateListDashboardViews = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListDashboardViewsQueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Persists the calling user's dashboard listing state (query, sort, order) as a named, reusable view shared across the org.
|
||||
* @summary Create dashboard saved view
|
||||
*/
|
||||
export const createDashboardView = (
|
||||
dashboardtypesPostableDashboardViewDTO?: BodyType<DashboardtypesPostableDashboardViewDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateDashboardView201>({
|
||||
url: `/api/v2/dashboards/views`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: dashboardtypesPostableDashboardViewDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateDashboardViewMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDashboardView>>,
|
||||
TError,
|
||||
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDashboardView>>,
|
||||
TError,
|
||||
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createDashboardView'];
|
||||
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 createDashboardView>>,
|
||||
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createDashboardView(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateDashboardViewMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createDashboardView>>
|
||||
>;
|
||||
export type CreateDashboardViewMutationBody =
|
||||
| BodyType<DashboardtypesPostableDashboardViewDTO>
|
||||
| undefined;
|
||||
export type CreateDashboardViewMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create dashboard saved view
|
||||
*/
|
||||
export const useCreateDashboardView = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createDashboardView>>,
|
||||
TError,
|
||||
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createDashboardView>>,
|
||||
TError,
|
||||
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateDashboardViewMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Removes a saved view. Saved views are shared org-wide; any user in the org may delete any view. Idempotent — deleting a non-existent view returns 404.
|
||||
* @summary Delete dashboard saved view
|
||||
*/
|
||||
export const deleteDashboardView = (
|
||||
{ id }: DeleteDashboardViewPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v2/dashboards/views/${id}`,
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteDashboardViewMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteDashboardView>>,
|
||||
TError,
|
||||
{ pathParams: DeleteDashboardViewPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteDashboardView>>,
|
||||
TError,
|
||||
{ pathParams: DeleteDashboardViewPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteDashboardView'];
|
||||
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 deleteDashboardView>>,
|
||||
{ pathParams: DeleteDashboardViewPathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteDashboardView(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteDashboardViewMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteDashboardView>>
|
||||
>;
|
||||
|
||||
export type DeleteDashboardViewMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Delete dashboard saved view
|
||||
*/
|
||||
export const useDeleteDashboardView = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteDashboardView>>,
|
||||
TError,
|
||||
{ pathParams: DeleteDashboardViewPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteDashboardView>>,
|
||||
TError,
|
||||
{ pathParams: DeleteDashboardViewPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getDeleteDashboardViewMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Replaces a saved view's name and data. Saved views are shared org-wide; any user in the org may edit any view.
|
||||
* @summary Update dashboard saved view
|
||||
*/
|
||||
export const updateDashboardView = (
|
||||
{ id }: UpdateDashboardViewPathParameters,
|
||||
dashboardtypesPostableDashboardViewDTO?: BodyType<DashboardtypesPostableDashboardViewDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<UpdateDashboardView200>({
|
||||
url: `/api/v2/dashboards/views/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: dashboardtypesPostableDashboardViewDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateDashboardViewMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateDashboardView>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDashboardViewPathParameters;
|
||||
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateDashboardView>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDashboardViewPathParameters;
|
||||
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateDashboardView'];
|
||||
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 updateDashboardView>>,
|
||||
{
|
||||
pathParams: UpdateDashboardViewPathParameters;
|
||||
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateDashboardView(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateDashboardViewMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateDashboardView>>
|
||||
>;
|
||||
export type UpdateDashboardViewMutationBody =
|
||||
| BodyType<DashboardtypesPostableDashboardViewDTO>
|
||||
| undefined;
|
||||
export type UpdateDashboardViewMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update dashboard saved view
|
||||
*/
|
||||
export const useUpdateDashboardView = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateDashboardView>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDashboardViewPathParameters;
|
||||
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateDashboardView>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateDashboardViewPathParameters;
|
||||
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateDashboardViewMutationOptions(options));
|
||||
};
|
||||
|
||||
@@ -2651,7 +2651,6 @@ export enum CloudintegrationtypesServiceIDDTO {
|
||||
sqs = 'sqs',
|
||||
storageaccountsblob = 'storageaccountsblob',
|
||||
cdnprofile = 'cdnprofile',
|
||||
aks = 'aks',
|
||||
}
|
||||
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
|
||||
/**
|
||||
@@ -4617,10 +4616,58 @@ export interface DashboardtypesDashboardSpecDTO {
|
||||
variables?: DashboardtypesVariableDTO[];
|
||||
}
|
||||
|
||||
export enum DashboardtypesListOrderDTO {
|
||||
asc = 'asc',
|
||||
desc = 'desc',
|
||||
}
|
||||
export enum DashboardtypesListSortDTO {
|
||||
updated_at = 'updated_at',
|
||||
created_at = 'created_at',
|
||||
name = 'name',
|
||||
}
|
||||
export interface DashboardtypesDashboardViewDataDTO {
|
||||
order?: DashboardtypesListOrderDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
query?: string;
|
||||
sort?: DashboardtypesListSortDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface DashboardtypesDashboardViewDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
data: DashboardtypesDashboardViewDataDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export enum DashboardtypesDatasourcePluginKindDTO {
|
||||
'signoz/Datasource' = 'signoz/Datasource',
|
||||
}
|
||||
export interface TagtypesPostableTagDTO {
|
||||
export interface TagtypesGettableTagDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -4670,7 +4717,7 @@ export interface DashboardtypesGettableDashboardV2DTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
tags: TagtypesPostableTagDTO[] | null;
|
||||
tags: TagtypesGettableTagDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -4728,6 +4775,88 @@ export interface DashboardtypesJSONPatchOperationDTO {
|
||||
value?: unknown;
|
||||
}
|
||||
|
||||
export interface DashboardtypesListedDashboardV2SpecDTO {
|
||||
display?: CommonDisplayDTO;
|
||||
}
|
||||
|
||||
export interface DashboardtypesListedDashboardV2DTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
image?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
locked: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
pinned: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
schemaVersion: string;
|
||||
source: DashboardtypesSourceDTO;
|
||||
spec: DashboardtypesListedDashboardV2SpecDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
tags: TagtypesGettableTagDTO[];
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface DashboardtypesListableDashboardV2DTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
dashboards: DashboardtypesListedDashboardV2DTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
tags: TagtypesGettableTagDTO[];
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface DashboardtypesListableDashboardViewDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
views: DashboardtypesDashboardViewDTO[];
|
||||
}
|
||||
|
||||
export enum DashboardtypesPanelPluginKindDTO {
|
||||
'signoz/TimeSeriesPanel' = 'signoz/TimeSeriesPanel',
|
||||
'signoz/BarChartPanel' = 'signoz/BarChartPanel',
|
||||
@@ -4744,6 +4873,17 @@ export type DashboardtypesPatchableDashboardV2DTO =
|
||||
| DashboardtypesJSONPatchOperationDTO[]
|
||||
| null;
|
||||
|
||||
export interface TagtypesPostableTagDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface DashboardtypesPostableDashboardV2DTO {
|
||||
/**
|
||||
* @type boolean
|
||||
@@ -4768,6 +4908,14 @@ export interface DashboardtypesPostableDashboardV2DTO {
|
||||
tags: TagtypesPostableTagDTO[] | null;
|
||||
}
|
||||
|
||||
export interface DashboardtypesPostableDashboardViewDTO {
|
||||
data: DashboardtypesDashboardViewDataDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DashboardtypesPostablePublicDashboardDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -7770,77 +7918,6 @@ export enum SpantypesFieldContextDTO {
|
||||
attribute = 'attribute',
|
||||
resource = 'resource',
|
||||
}
|
||||
export type SpantypesFlamegraphSpanDTOAttributes = { [key: string]: unknown };
|
||||
|
||||
export type SpantypesFlamegraphSpanDTOResource = { [key: string]: string };
|
||||
|
||||
export interface SpantypesFlamegraphSpanDTO {
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
attributes: SpantypesFlamegraphSpanDTOAttributes;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
durationNano: number;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
event: SpantypesEventDTO[];
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
hasError: boolean;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
level: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
parentSpanId: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
resource: SpantypesFlamegraphSpanDTOResource;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
spanId: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface SpantypesGettableFlamegraphTraceDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
endTimestampMillis: number;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
hasMore: boolean;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
spans: SpantypesFlamegraphSpanDTO[][];
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
startTimestampMillis: number;
|
||||
}
|
||||
|
||||
export type SpantypesSpanMapperGroupConditionDTOAnyOf = {
|
||||
/**
|
||||
* @type array,null
|
||||
@@ -8142,17 +8219,6 @@ export interface SpantypesGettableWaterfallTraceDTO {
|
||||
uncollapsedSpans?: string[] | null;
|
||||
}
|
||||
|
||||
export interface SpantypesPostableFlamegraphDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
selectedSpanId?: string;
|
||||
}
|
||||
|
||||
export enum SpantypesSpanMapperOperationDTO {
|
||||
move = 'move',
|
||||
copy = 'copy',
|
||||
@@ -9659,6 +9725,40 @@ export type GetUserPreference200 = {
|
||||
export type UpdateUserPreferencePathParameters = {
|
||||
name: string;
|
||||
};
|
||||
export type ListDashboardsV2Params = {
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
query?: string;
|
||||
/**
|
||||
* @description undefined
|
||||
*/
|
||||
sort?: DashboardtypesListSortDTO;
|
||||
/**
|
||||
* @description undefined
|
||||
*/
|
||||
order?: DashboardtypesListOrderDTO;
|
||||
/**
|
||||
* @type integer
|
||||
* @description undefined
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @description undefined
|
||||
*/
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type ListDashboardsV2200 = {
|
||||
data: DashboardtypesListableDashboardV2DTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateDashboardV2201 = {
|
||||
data: DashboardtypesGettableDashboardV2DTO;
|
||||
/**
|
||||
@@ -9667,6 +9767,9 @@ export type CreateDashboardV2201 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteDashboardV2PathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetDashboardV2PathParameters = {
|
||||
id: string;
|
||||
};
|
||||
@@ -9706,6 +9809,42 @@ export type UnlockDashboardV2PathParameters = {
|
||||
export type LockDashboardV2PathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type UnpinDashboardV2PathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type PinDashboardV2PathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type ListDashboardViews200 = {
|
||||
data: DashboardtypesListableDashboardViewDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateDashboardView201 = {
|
||||
data: DashboardtypesDashboardViewDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteDashboardViewPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type UpdateDashboardViewPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type UpdateDashboardView200 = {
|
||||
data: DashboardtypesDashboardViewDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetFeatures200 = {
|
||||
/**
|
||||
* @type array
|
||||
@@ -10507,17 +10646,6 @@ export type GetHosts200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetFlamegraphPathParameters = {
|
||||
traceID: string;
|
||||
};
|
||||
export type GetFlamegraph200 = {
|
||||
data: SpantypesGettableFlamegraphTraceDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetWaterfallPathParameters = {
|
||||
traceID: string;
|
||||
};
|
||||
|
||||
@@ -12,8 +12,6 @@ import type {
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
GetFlamegraph200,
|
||||
GetFlamegraphPathParameters,
|
||||
GetTraceAggregations200,
|
||||
GetTraceAggregationsPathParameters,
|
||||
GetWaterfall200,
|
||||
@@ -21,7 +19,6 @@ import type {
|
||||
GetWaterfallV4200,
|
||||
GetWaterfallV4PathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
SpantypesPostableFlamegraphDTO,
|
||||
SpantypesPostableTraceAggregationsDTO,
|
||||
SpantypesPostableWaterfallDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
@@ -129,105 +126,6 @@ export const useGetTraceAggregations = <
|
||||
> => {
|
||||
return useMutation(getGetTraceAggregationsMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Returns the flamegraph view of spans for a given trace ID.
|
||||
* @summary Get flamegraph view for a trace
|
||||
*/
|
||||
export const getFlamegraph = (
|
||||
{ traceID }: GetFlamegraphPathParameters,
|
||||
spantypesPostableFlamegraphDTO?: BodyType<SpantypesPostableFlamegraphDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetFlamegraph200>({
|
||||
url: `/api/v3/traces/${traceID}/flamegraph`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: spantypesPostableFlamegraphDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetFlamegraphMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof getFlamegraph>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: GetFlamegraphPathParameters;
|
||||
data?: BodyType<SpantypesPostableFlamegraphDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof getFlamegraph>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: GetFlamegraphPathParameters;
|
||||
data?: BodyType<SpantypesPostableFlamegraphDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['getFlamegraph'];
|
||||
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 getFlamegraph>>,
|
||||
{
|
||||
pathParams: GetFlamegraphPathParameters;
|
||||
data?: BodyType<SpantypesPostableFlamegraphDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return getFlamegraph(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type GetFlamegraphMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getFlamegraph>>
|
||||
>;
|
||||
export type GetFlamegraphMutationBody =
|
||||
| BodyType<SpantypesPostableFlamegraphDTO>
|
||||
| undefined;
|
||||
export type GetFlamegraphMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get flamegraph view for a trace
|
||||
*/
|
||||
export const useGetFlamegraph = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof getFlamegraph>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: GetFlamegraphPathParameters;
|
||||
data?: BodyType<SpantypesPostableFlamegraphDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof getFlamegraph>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: GetFlamegraphPathParameters;
|
||||
data?: BodyType<SpantypesPostableFlamegraphDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getGetFlamegraphMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Returns the waterfall view of spans for a given trace ID with tree structure, metadata, and windowed pagination
|
||||
* @summary Get waterfall view for a trace
|
||||
|
||||
@@ -72,7 +72,7 @@ export const deploymentWidgetInfo = [
|
||||
yAxisUnit: '',
|
||||
},
|
||||
{
|
||||
title: 'Memory usage, request, limits',
|
||||
title: 'Memory usage, request, limits)',
|
||||
yAxisUnit: 'bytes',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -69,7 +69,7 @@ export const jobWidgetInfo = [
|
||||
yAxisUnit: '',
|
||||
},
|
||||
{
|
||||
title: 'Memory Usage',
|
||||
title: 'Memory usage, request, limits',
|
||||
yAxisUnit: 'bytes',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -703,7 +703,7 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
],
|
||||
having: [],
|
||||
legend: `{{${k8sPodNameKey}}}`,
|
||||
limit: 10,
|
||||
limit: 20,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
@@ -1014,8 +1014,8 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
id: '5f2a55c5',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: k8sNamespaceNameKey,
|
||||
key: k8sNamespaceNameKey,
|
||||
id: k8sStatefulsetNameKey,
|
||||
key: k8sStatefulsetNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
|
||||
@@ -317,9 +317,9 @@ export const getVolumeMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_volume_inodes_used--float64--Gauge--true',
|
||||
id: 'k8s_volume_inodes_used--float64----true',
|
||||
key: k8sVolumeInodesUsedKey,
|
||||
type: 'Gauge',
|
||||
type: '',
|
||||
},
|
||||
aggregateOperator: 'avg',
|
||||
dataSource: DataSource.METRICS,
|
||||
@@ -409,9 +409,9 @@ export const getVolumeMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_volume_inodes--float64--Gauge--true',
|
||||
id: 'k8s_volume_inodes--float64----true',
|
||||
key: k8sVolumeInodesKey,
|
||||
type: 'Gauge',
|
||||
type: '',
|
||||
},
|
||||
aggregateOperator: 'avg',
|
||||
dataSource: DataSource.METRICS,
|
||||
@@ -501,9 +501,9 @@ export const getVolumeMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_volume_inodes_free--float64--Gauge--true',
|
||||
id: 'k8s_volume_inodes_free--float64----true',
|
||||
key: k8sVolumeInodesFreeKey,
|
||||
type: 'Gauge',
|
||||
type: '',
|
||||
},
|
||||
aggregateOperator: 'avg',
|
||||
dataSource: DataSource.METRICS,
|
||||
|
||||
@@ -1619,9 +1619,6 @@ export const getHostQueryPayload = (
|
||||
const diskOpTimeKey = dotMetricsEnabled
|
||||
? 'system.disk.operation_time'
|
||||
: 'system_disk_operation_time';
|
||||
const diskOpsKey = dotMetricsEnabled
|
||||
? 'system.disk.operations'
|
||||
: 'system_disk_operations';
|
||||
const diskPendingKey = dotMetricsEnabled
|
||||
? 'system.disk.pending_operations'
|
||||
: 'system_disk_pending_operations';
|
||||
@@ -2378,24 +2375,9 @@ export const getHostQueryPayload = (
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'direction--string--tag--false',
|
||||
|
||||
key: 'direction',
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'device--string--tag--false',
|
||||
|
||||
key: 'device',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
groupBy: [],
|
||||
having: [],
|
||||
legend: '{{device}}::{{direction}}',
|
||||
legend: 'system disk io',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
@@ -2427,9 +2409,9 @@ export const getHostQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'system_disk_operations--float64--Sum--true',
|
||||
id: 'system_disk_operation_time--float64--Sum--true',
|
||||
|
||||
key: diskOpsKey,
|
||||
key: diskOpTimeKey,
|
||||
type: 'Sum',
|
||||
},
|
||||
aggregateOperator: 'rate',
|
||||
@@ -2439,7 +2421,7 @@ export const getHostQueryPayload = (
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'diskops_f1',
|
||||
id: 'diskop_f1',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'host_name--string--tag--false',
|
||||
@@ -2472,7 +2454,7 @@ export const getHostQueryPayload = (
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${diskOpsKey})`,
|
||||
columnName: `SUM(${diskOpTimeKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
@@ -2575,88 +2557,6 @@ export const getHostQueryPayload = (
|
||||
start,
|
||||
end,
|
||||
},
|
||||
{
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
query: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'system_disk_operation_time--float64--Sum--true',
|
||||
|
||||
key: diskOpTimeKey,
|
||||
type: 'Sum',
|
||||
},
|
||||
aggregateOperator: 'rate',
|
||||
dataSource: DataSource.METRICS,
|
||||
disabled: false,
|
||||
expression: 'A',
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'diskoptime_f1',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'host_name--string--tag--false',
|
||||
|
||||
key: hostNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: hostName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'device--string--tag--false',
|
||||
|
||||
key: 'device',
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'direction--string--tag--false',
|
||||
|
||||
key: 'direction',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${diskOpTimeKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
legend: '{{device}}::{{direction}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'rate',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: 'a8b3d2e1-4f5c-4a6b-9c8d-7e2f1a0b3c4f',
|
||||
promql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
},
|
||||
variables: {},
|
||||
formatForWeb: false,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -2731,5 +2631,5 @@ export const hostWidgetInfo = [
|
||||
{ title: 'System disk io (bytes transferred)', yAxisUnit: 'bytes' },
|
||||
{ title: 'System disk operations/s', yAxisUnit: 'short' },
|
||||
{ title: 'Queue size', yAxisUnit: 'short' },
|
||||
{ title: 'System disk operation time/s', yAxisUnit: 's' },
|
||||
{ title: 'Disk operations time', yAxisUnit: 's' },
|
||||
];
|
||||
|
||||
@@ -1,36 +1,29 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FullScreenHandle } from 'react-full-screen';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import {
|
||||
ClipboardCopy,
|
||||
Configure,
|
||||
Ellipsis,
|
||||
FileJson,
|
||||
Fullscreen,
|
||||
LockKeyhole,
|
||||
PenLine,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from '@signozhq/icons';
|
||||
import { Popover } from 'antd';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||
import type { MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DeleteButton } from 'container/ListOfDashboard/TableComponents/DeleteButton';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard';
|
||||
import history from 'lib/history';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import ConfirmDeleteDialog from '../../components/ConfirmDeleteDialog/ConfirmDeleteDialog';
|
||||
import DashboardSettings from '../../DashboardSettings';
|
||||
import SettingsDrawer from '../SettingsDrawer';
|
||||
import styles from '../DashboardDescription.module.scss';
|
||||
|
||||
interface DashboardActionsProps {
|
||||
interface Props {
|
||||
dashboard: DashboardtypesGettableDashboardV2DTO;
|
||||
handle: FullScreenHandle;
|
||||
isDashboardLocked: boolean;
|
||||
@@ -52,19 +45,17 @@ function DashboardActions({
|
||||
onAddPanel,
|
||||
onLockToggle,
|
||||
onOpenRename,
|
||||
}: DashboardActionsProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const { t } = useTranslation(['dashboard', 'common']);
|
||||
|
||||
const id = dashboard.id ?? '';
|
||||
const id = dashboard.id;
|
||||
const title = dashboard.spec?.display?.name ?? '';
|
||||
|
||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] =
|
||||
const [isDashboardSettingsOpen, setIsDashboardSettingsOpen] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const [state, setCopy] = useCopyToClipboard();
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState<boolean>(false);
|
||||
const deleteDashboardMutation = useDeleteDashboard(id);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.error) {
|
||||
@@ -75,12 +66,9 @@ function DashboardActions({
|
||||
}
|
||||
}, [state.error, state.value, t]);
|
||||
|
||||
const dashboardDataJSON = useCallback(
|
||||
(): string => JSON.stringify(dashboard, null, 2),
|
||||
[dashboard],
|
||||
);
|
||||
const dashboardDataJSON = (): string => JSON.stringify(dashboard, null, 2);
|
||||
|
||||
const exportJSON = useCallback((): void => {
|
||||
const exportJSON = (): void => {
|
||||
const blob = new Blob([dashboardDataJSON()], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
@@ -90,141 +78,119 @@ function DashboardActions({
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}, [dashboardDataJSON, title]);
|
||||
|
||||
const handleConfirmDelete = useCallback((): void => {
|
||||
deleteDashboardMutation.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
setIsDeleteOpen(false);
|
||||
history.replace(ROUTES.ALL_DASHBOARD);
|
||||
},
|
||||
});
|
||||
}, [deleteDashboardMutation]);
|
||||
|
||||
const menuItems = useMemo<MenuItem[]>(() => {
|
||||
const editGroup: MenuItem[] = [];
|
||||
if (!isDashboardLocked && editDashboard) {
|
||||
editGroup.push({
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
icon: <PenLine size={14} />,
|
||||
onClick: onOpenRename,
|
||||
});
|
||||
}
|
||||
if (isAuthor || user.role === USER_ROLES.ADMIN) {
|
||||
editGroup.push({
|
||||
key: 'lock',
|
||||
label: isDashboardLocked ? 'Unlock dashboard' : 'Lock dashboard',
|
||||
icon: <LockKeyhole size={14} />,
|
||||
disabled: dashboard.createdBy === 'integration',
|
||||
onClick: onLockToggle,
|
||||
});
|
||||
}
|
||||
editGroup.push({
|
||||
key: 'fullscreen',
|
||||
label: 'Full screen',
|
||||
icon: <Fullscreen size={14} />,
|
||||
onClick: handle.enter,
|
||||
});
|
||||
|
||||
const exportGroup: MenuItem[] = [
|
||||
{
|
||||
key: 'export',
|
||||
label: 'Export JSON',
|
||||
icon: <FileJson size={14} />,
|
||||
onClick: exportJSON,
|
||||
},
|
||||
{
|
||||
key: 'copy',
|
||||
label: 'Copy as JSON',
|
||||
icon: <ClipboardCopy size={14} />,
|
||||
onClick: (): void => setCopy(dashboardDataJSON()),
|
||||
},
|
||||
];
|
||||
|
||||
const dangerGroup: MenuItem[] = [
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete dashboard',
|
||||
icon: <Trash2 size={14} />,
|
||||
danger: true,
|
||||
onClick: (): void => setIsDeleteOpen(true),
|
||||
},
|
||||
];
|
||||
|
||||
return [editGroup, exportGroup, dangerGroup]
|
||||
.filter((group) => group.length > 0)
|
||||
.flatMap((group, index) =>
|
||||
index > 0 ? [{ type: 'divider' } as MenuItem, ...group] : group,
|
||||
);
|
||||
}, [
|
||||
isDashboardLocked,
|
||||
editDashboard,
|
||||
isAuthor,
|
||||
user.role,
|
||||
dashboard.createdBy,
|
||||
onOpenRename,
|
||||
onLockToggle,
|
||||
handle.enter,
|
||||
exportJSON,
|
||||
setCopy,
|
||||
dashboardDataJSON,
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.rightSection}>
|
||||
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
|
||||
<DropdownMenuSimple menu={{ items: menuItems }}>
|
||||
<Popover
|
||||
open={isDashboardSettingsOpen}
|
||||
arrow={false}
|
||||
onOpenChange={(visible): void => setIsDashboardSettingsOpen(visible)}
|
||||
rootClassName={styles.dashboardSettings}
|
||||
content={
|
||||
<div className={styles.menuContent}>
|
||||
<section className={styles.section1}>
|
||||
{(isAuthor || user.role === USER_ROLES.ADMIN) && (
|
||||
<TooltipSimple
|
||||
title={
|
||||
dashboard.createdBy === 'integration'
|
||||
? 'Dashboards created by integrations cannot be unlocked'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
prefix={<LockKeyhole size={14} />}
|
||||
disabled={dashboard.createdBy === 'integration'}
|
||||
onClick={(): void => {
|
||||
setIsDashboardSettingsOpen(false);
|
||||
onLockToggle();
|
||||
}}
|
||||
testId="lock-unlock-dashboard"
|
||||
>
|
||||
{isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
|
||||
{!isDashboardLocked && editDashboard && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
prefix={<PenLine size={14} />}
|
||||
onClick={(): void => {
|
||||
onOpenRename();
|
||||
setIsDashboardSettingsOpen(false);
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
prefix={<Fullscreen size={14} />}
|
||||
onClick={handle.enter}
|
||||
>
|
||||
Full screen
|
||||
</Button>
|
||||
</section>
|
||||
<section className={styles.section2}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
prefix={<FileJson size={14} />}
|
||||
onClick={(): void => {
|
||||
exportJSON();
|
||||
setIsDashboardSettingsOpen(false);
|
||||
}}
|
||||
>
|
||||
Export JSON
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
prefix={<ClipboardCopy size={14} />}
|
||||
onClick={(): void => {
|
||||
setCopy(dashboardDataJSON());
|
||||
setIsDashboardSettingsOpen(false);
|
||||
}}
|
||||
>
|
||||
Copy as JSON
|
||||
</Button>
|
||||
</section>
|
||||
<section className={styles.deleteDashboard}>
|
||||
<DeleteButton
|
||||
createdBy={dashboard.createdBy || ''}
|
||||
name={title}
|
||||
id={id}
|
||||
isLocked={isDashboardLocked}
|
||||
routeToListPage
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
prefix={<Ellipsis size={14} />}
|
||||
className={styles.icons}
|
||||
testId="options"
|
||||
/>
|
||||
</DropdownMenuSimple>
|
||||
{!isDashboardLocked && editDashboard && (
|
||||
<>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
prefix={<Configure size="md" />}
|
||||
testId="show-drawer"
|
||||
onClick={(): void => setIsSettingsDrawerOpen(true)}
|
||||
size="md"
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
<SettingsDrawer
|
||||
drawerTitle="Dashboard Configuration"
|
||||
isOpen={isSettingsDrawerOpen}
|
||||
onClose={(): void => setIsSettingsDrawerOpen(false)}
|
||||
>
|
||||
<DashboardSettings dashboard={dashboard} />
|
||||
</SettingsDrawer>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
{!isDashboardLocked && addPanelPermission && (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
className={styles.addPanelBtn}
|
||||
onClick={onAddPanel}
|
||||
prefix={<Plus size="md" />}
|
||||
testId="add-panel-header"
|
||||
size="md"
|
||||
>
|
||||
New Panel
|
||||
</Button>
|
||||
)}
|
||||
<ConfirmDeleteDialog
|
||||
open={isDeleteOpen}
|
||||
title={`Delete dashboard "${title}"?`}
|
||||
description="This action cannot be undone."
|
||||
isLoading={deleteDashboardMutation.isLoading}
|
||||
onConfirm={handleConfirmDelete}
|
||||
onClose={(): void => setIsDeleteOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 45%;
|
||||
height: 40px;
|
||||
|
||||
.dashboardImg {
|
||||
height: 16px;
|
||||
@@ -43,35 +42,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.clickableTitle {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.titleEdit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.titleInput {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.titleEditActionButton {
|
||||
--button-height: auto;
|
||||
--button-padding: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.titleSaveActionButton {
|
||||
--button-border-color: var(--text-forest-700);
|
||||
--button-outlined-foreground: var(--text-forest-700);
|
||||
}
|
||||
|
||||
.publicDashboardIcon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
@@ -84,7 +54,6 @@
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
height: 40px;
|
||||
|
||||
.icons {
|
||||
display: flex;
|
||||
@@ -108,6 +77,41 @@
|
||||
.icons:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.configureButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 93px;
|
||||
height: 34px;
|
||||
padding: 6px;
|
||||
justify-content: center;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 10px; /* 83.333% */
|
||||
letter-spacing: 0.12px;
|
||||
}
|
||||
|
||||
.addPanelBtn {
|
||||
display: flex;
|
||||
width: 119px;
|
||||
height: 34px;
|
||||
padding: 5.937px 11.875px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--primary-foreground);
|
||||
background: var(--primary-background);
|
||||
font-family: Inter;
|
||||
font-size: 11.875px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 17.812px; /* 150% */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +209,95 @@
|
||||
}
|
||||
}
|
||||
|
||||
.deleteModal :global(.ant-modal-confirm-body) {
|
||||
align-items: center;
|
||||
.renameDashboard {
|
||||
:global(.ant-modal-content) {
|
||||
width: 384px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
padding: 0px;
|
||||
|
||||
:global(.ant-modal-header) {
|
||||
height: 52px;
|
||||
padding: 16px;
|
||||
background: var(--l2-background);
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
margin-bottom: 0px;
|
||||
|
||||
:global(.ant-modal-title) {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
width: 349px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-modal-body) {
|
||||
padding: 16px;
|
||||
|
||||
.dashboardContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.nameText {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
}
|
||||
|
||||
.dashboardNameInput {
|
||||
display: flex;
|
||||
padding: 6px 6px 6px 8px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
align-self: stretch;
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-modal-footer) {
|
||||
padding: 16px;
|
||||
margin-top: 0px;
|
||||
|
||||
.dashboardRename {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
gap: 12px;
|
||||
|
||||
.cancelBtn {
|
||||
display: flex;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--l1-border);
|
||||
}
|
||||
|
||||
.renameBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 169px;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--primary-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import { isEmpty } from 'lodash-es';
|
||||
|
||||
import styles from '../DashboardDescription.module.scss';
|
||||
|
||||
interface DashboardMetaProps {
|
||||
interface Props {
|
||||
tags: string[];
|
||||
description: string;
|
||||
}
|
||||
|
||||
function DashboardMeta({ tags, description }: DashboardMetaProps): JSX.Element {
|
||||
function DashboardMeta({ tags, description }: Props): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{tags.length > 0 && (
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
import { KeyboardEvent } from 'react';
|
||||
import { Check, Globe, LockKeyhole, X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Globe, LockKeyhole } from '@signozhq/icons';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
|
||||
import styles from '../DashboardDescription.module.scss';
|
||||
|
||||
interface DashboardTitleProps {
|
||||
interface Props {
|
||||
title: string;
|
||||
image: string;
|
||||
isPublicDashboard: boolean;
|
||||
isDashboardLocked: boolean;
|
||||
isEditable: boolean;
|
||||
isEditing: boolean;
|
||||
draft: string;
|
||||
onDraftChange: (value: string) => void;
|
||||
onStartEdit: () => void;
|
||||
onCommit: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
function DashboardTitle({
|
||||
@@ -27,76 +16,18 @@ function DashboardTitle({
|
||||
image,
|
||||
isPublicDashboard,
|
||||
isDashboardLocked,
|
||||
isEditable,
|
||||
isEditing,
|
||||
draft,
|
||||
onDraftChange,
|
||||
onStartEdit,
|
||||
onCommit,
|
||||
onCancel,
|
||||
}: DashboardTitleProps): JSX.Element {
|
||||
const canEdit = isEditable && !isDashboardLocked;
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
onCommit();
|
||||
} else if (event.key === 'Escape') {
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<div className={styles.leftSection}>
|
||||
<img src={image} alt="dashboard-img" className={styles.dashboardImg} />
|
||||
{isEditing ? (
|
||||
<div className={styles.titleEdit}>
|
||||
<Input
|
||||
autoFocus
|
||||
value={draft}
|
||||
testId="dashboard-title-input"
|
||||
maxLength={120}
|
||||
className={styles.titleInput}
|
||||
onChange={(e): void => onDraftChange(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
size="icon"
|
||||
className={cx(styles.titleEditActionButton, styles.titleSaveActionButton)}
|
||||
aria-label="Save title"
|
||||
testId="dashboard-title-save"
|
||||
onClick={onCommit}
|
||||
>
|
||||
<Check size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="destructive"
|
||||
size="icon"
|
||||
className={styles.titleEditActionButton}
|
||||
aria-label="Cancel title edit"
|
||||
testId="dashboard-title-cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<TooltipSimple title={title.length > 30 ? title : ''}>
|
||||
<Typography.Text
|
||||
className={cx(styles.dashboardTitle, {
|
||||
[styles.clickableTitle]: canEdit,
|
||||
})}
|
||||
data-testid="dashboard-title"
|
||||
onClick={canEdit ? onStartEdit : undefined}
|
||||
>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
<TooltipSimple title={title.length > 30 ? title : ''}>
|
||||
<Typography.Text
|
||||
className={styles.dashboardTitle}
|
||||
data-testid="dashboard-title"
|
||||
>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
</TooltipSimple>
|
||||
|
||||
{isPublicDashboard && (
|
||||
<TooltipSimple title="This dashboard is publicly accessible">
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface UseEditableTitleArgs {
|
||||
value: string;
|
||||
onSave: (next: string) => void;
|
||||
}
|
||||
|
||||
interface UseEditableTitleResult {
|
||||
isEditing: boolean;
|
||||
draft: string;
|
||||
setDraft: (next: string) => void;
|
||||
startEdit: () => void;
|
||||
cancel: () => void;
|
||||
commit: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drives an inline-editable title. The parent owns the canonical `value`; this
|
||||
* hook tracks the in-flight `draft` and whether we're editing. `commit` saves
|
||||
* only when the trimmed draft is non-empty and actually changed. A `cancelled`
|
||||
* ref guards against a blur firing right after Escape from also committing.
|
||||
*/
|
||||
export function useEditableTitle({
|
||||
value,
|
||||
onSave,
|
||||
}: UseEditableTitleArgs): UseEditableTitleResult {
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [draft, setDraft] = useState<string>(value);
|
||||
const cancelled = useRef<boolean>(false);
|
||||
|
||||
// Keep the draft in sync with the canonical value while not editing (e.g.
|
||||
// after a refetch updates the title).
|
||||
useEffect(() => {
|
||||
if (!isEditing) {
|
||||
setDraft(value);
|
||||
}
|
||||
}, [value, isEditing]);
|
||||
|
||||
const startEdit = (): void => {
|
||||
cancelled.current = false;
|
||||
setDraft(value);
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const cancel = (): void => {
|
||||
cancelled.current = true;
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const commit = (): void => {
|
||||
if (cancelled.current) {
|
||||
cancelled.current = false;
|
||||
return;
|
||||
}
|
||||
const trimmed = draft.trim();
|
||||
if (trimmed && trimmed !== value) {
|
||||
onSave(trimmed);
|
||||
}
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
return { isEditing, draft, setDraft, startEdit, cancel, commit };
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Input, Modal } from 'antd';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Check, X } from '@signozhq/icons';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import styles from '../DashboardDescription.module.scss';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
value: string;
|
||||
isLoading: boolean;
|
||||
onChange: (value: string) => void;
|
||||
onRename: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function RenameDashboardModal({
|
||||
open,
|
||||
value,
|
||||
isLoading,
|
||||
onChange,
|
||||
onRename,
|
||||
onClose,
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title="Rename Dashboard"
|
||||
onOk={onRename}
|
||||
onCancel={onClose}
|
||||
rootClassName={styles.renameDashboard}
|
||||
footer={
|
||||
<div className={styles.dashboardRename}>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
prefix={<Check size={14} />}
|
||||
className={styles.renameBtn}
|
||||
onClick={onRename}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Rename Dashboard
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
prefix={<X size={14} />}
|
||||
className={styles.cancelBtn}
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={styles.dashboardContent}>
|
||||
<Typography.Text className={styles.nameText}>
|
||||
Enter a new name
|
||||
</Typography.Text>
|
||||
<Input
|
||||
data-testid="dashboard-name"
|
||||
className={styles.dashboardNameInput}
|
||||
value={value}
|
||||
onChange={(e): void => onChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default RenameDashboardModal;
|
||||
@@ -1,43 +0,0 @@
|
||||
.settingsContainerRoot {
|
||||
:global(.ant-drawer-wrapper-body) {
|
||||
border-left: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
|
||||
:global(.ant-drawer-header) {
|
||||
height: 48px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
padding: 14px 14px 14px 11px;
|
||||
|
||||
:global(.ant-drawer-header-title) {
|
||||
gap: 16px;
|
||||
|
||||
:global(.ant-drawer-title) {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
:global(.ant-drawer-close) {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-inline-end: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-drawer-body) {
|
||||
padding: 16px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { memo, PropsWithChildren, ReactElement } from 'react';
|
||||
import { Drawer } from 'antd';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
|
||||
import styles from './SettingsDrawer.module.scss';
|
||||
|
||||
type SettingsDrawerProps = PropsWithChildren<{
|
||||
drawerTitle: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}>;
|
||||
|
||||
function SettingsDrawer({
|
||||
children,
|
||||
drawerTitle,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: SettingsDrawerProps): JSX.Element {
|
||||
return (
|
||||
<Drawer
|
||||
title={drawerTitle}
|
||||
placement="right"
|
||||
width="50%"
|
||||
onClose={onClose}
|
||||
open={isOpen}
|
||||
rootClassName={styles.settingsContainerRoot}
|
||||
>
|
||||
{/* Need to type cast because of OverlayScrollbar type definition. We should be good once we remove it. */}
|
||||
<OverlayScrollbar>{children as ReactElement}</OverlayScrollbar>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(SettingsDrawer);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { FullScreenHandle } from 'react-full-screen';
|
||||
import { Card } from 'antd';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
@@ -15,7 +15,6 @@ import type {
|
||||
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
@@ -23,7 +22,7 @@ import DashboardHeader from '../components/DashboardHeader/DashboardHeader';
|
||||
import DashboardActions from './DashboardActions/DashboardActions';
|
||||
import DashboardMeta from './DashboardMeta/DashboardMeta';
|
||||
import DashboardTitle from './DashboardTitle/DashboardTitle';
|
||||
import { useEditableTitle } from './DashboardTitle/useEditableTitle';
|
||||
import RenameDashboardModal from './RenameDashboardModal/RenameDashboardModal';
|
||||
|
||||
import styles from './DashboardDescription.module.scss';
|
||||
|
||||
@@ -53,9 +52,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const [editDashboard] = useComponentPermission(['edit_dashboard'], user.role);
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
|
||||
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||
);
|
||||
|
||||
const isAuthor =
|
||||
!!user?.email && !!dashboard.createdBy && dashboard.createdBy === user.email;
|
||||
@@ -63,7 +59,16 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
// V2 public dashboard wiring lives separately; treat as not-public for chrome.
|
||||
const isPublicDashboard = false;
|
||||
|
||||
const handleLockDashboardToggle = useCallback(async (): Promise<void> => {
|
||||
const [isRenameDashboardOpen, setIsRenameDashboardOpen] =
|
||||
useState<boolean>(false);
|
||||
const [updatedTitle, setUpdatedTitle] = useState<string>(title);
|
||||
const [isRenameLoading, setIsRenameLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setUpdatedTitle(title);
|
||||
}, [title]);
|
||||
|
||||
const handleLockDashboardToggle = async (): Promise<void> => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
@@ -79,43 +84,41 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
}
|
||||
}, [id, isDashboardLocked, refetch, showErrorModal]);
|
||||
};
|
||||
|
||||
const onNameSave = useCallback(
|
||||
async (next: string): Promise<void> => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const patch: DashboardtypesJSONPatchOperationDTO[] = [
|
||||
{
|
||||
op: 'replace' as DashboardtypesJSONPatchOperationDTO['op'],
|
||||
path: '/spec/display/name',
|
||||
value: next,
|
||||
},
|
||||
];
|
||||
await patchDashboardV2({ id }, patch);
|
||||
toast.success('Dashboard renamed successfully');
|
||||
refetch();
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
}
|
||||
},
|
||||
[id, refetch, showErrorModal],
|
||||
);
|
||||
const onNameChangeHandler = async (): Promise<void> => {
|
||||
const trimmed = updatedTitle.trim();
|
||||
if (!id || !trimmed || trimmed === title) {
|
||||
setIsRenameDashboardOpen(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsRenameLoading(true);
|
||||
const patch: DashboardtypesJSONPatchOperationDTO[] = [
|
||||
{
|
||||
op: 'replace' as DashboardtypesJSONPatchOperationDTO['op'],
|
||||
path: '/spec/display/name',
|
||||
value: trimmed,
|
||||
},
|
||||
];
|
||||
await patchDashboardV2({ id }, patch);
|
||||
toast.success('Dashboard renamed successfully');
|
||||
setIsRenameDashboardOpen(false);
|
||||
refetch();
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
setIsRenameDashboardOpen(true);
|
||||
} finally {
|
||||
setIsRenameLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const { isEditing, draft, setDraft, startEdit, cancel, commit } =
|
||||
useEditableTitle({
|
||||
value: title,
|
||||
onSave: onNameSave,
|
||||
});
|
||||
|
||||
const onEmptyWidgetHandler = useCallback((): void => {
|
||||
const onEmptyWidgetHandler = (): void => {
|
||||
void logEvent('Dashboard Detail V2: Add new panel clicked', {
|
||||
dashboardId: id,
|
||||
});
|
||||
setIsPanelTypeSelectionModalOpen(true);
|
||||
}, [id, setIsPanelTypeSelectionModalOpen]);
|
||||
toast.info('V2 panel editor coming next');
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={styles.dashboardDescriptionContainer}>
|
||||
@@ -126,13 +129,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
image={image}
|
||||
isPublicDashboard={isPublicDashboard}
|
||||
isDashboardLocked={isDashboardLocked}
|
||||
isEditable={editDashboard}
|
||||
isEditing={isEditing}
|
||||
draft={draft}
|
||||
onDraftChange={setDraft}
|
||||
onStartEdit={startEdit}
|
||||
onCommit={commit}
|
||||
onCancel={cancel}
|
||||
/>
|
||||
<DashboardActions
|
||||
dashboard={dashboard}
|
||||
@@ -143,10 +139,19 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
addPanelPermission={addPanelPermission}
|
||||
onAddPanel={onEmptyWidgetHandler}
|
||||
onLockToggle={handleLockDashboardToggle}
|
||||
onOpenRename={startEdit}
|
||||
onOpenRename={(): void => setIsRenameDashboardOpen(true)}
|
||||
/>
|
||||
</section>
|
||||
<DashboardMeta tags={tags} description={description} />
|
||||
|
||||
<RenameDashboardModal
|
||||
open={isRenameDashboardOpen}
|
||||
value={updatedTitle}
|
||||
isLoading={isRenameLoading}
|
||||
onChange={setUpdatedTitle}
|
||||
onRename={onNameChangeHandler}
|
||||
onClose={(): void => setIsRenameDashboardOpen(false)}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.placeholder {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.tabLabel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
line-height: 1;
|
||||
padding-top: 4px;
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
// eslint-disable-next-line signoz/no-antd-components -- TODO: migrate Radio to @signozhq/ui/radio-group
|
||||
import { Col, Radio, Tooltip } from 'antd';
|
||||
import { ExternalLink, SolidInfoCircle } from '@signozhq/icons';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { Events } from 'constants/events';
|
||||
import { useDashboardCursorSyncMode } from 'hooks/dashboard/useDashboardCursorSyncMode';
|
||||
import { useSyncTooltipFilterMode } from 'hooks/dashboard/useSyncTooltipFilterMode';
|
||||
import {
|
||||
DashboardCursorSync,
|
||||
SyncTooltipFilterMode,
|
||||
} from 'lib/uPlotV2/plugins/TooltipPlugin/types';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
import cx from 'classnames';
|
||||
|
||||
import styles from '../GeneralSettings.module.scss';
|
||||
|
||||
interface CrossPanelSyncProps {
|
||||
dashboardId: string;
|
||||
}
|
||||
|
||||
function CrossPanelSync({ dashboardId }: CrossPanelSyncProps): JSX.Element {
|
||||
const [cursorSyncMode, setCursorSyncMode] =
|
||||
useDashboardCursorSyncMode(dashboardId);
|
||||
const [syncTooltipFilterMode, setSyncTooltipFilterMode] =
|
||||
useSyncTooltipFilterMode(dashboardId);
|
||||
|
||||
return (
|
||||
<Col className={cx(styles.overviewSettings, styles.crossPanelSyncGroup)}>
|
||||
<div className={styles.crossPanelSyncSectionHeader}>
|
||||
<Typography.Text className={styles.crossPanelSyncSectionTitle}>
|
||||
Cross-Panel Sync
|
||||
</Typography.Text>
|
||||
<Tooltip
|
||||
title={
|
||||
<div className={styles.crossPanelSyncTooltipContent}>
|
||||
<strong className={styles.crossPanelSyncTooltipTitle}>
|
||||
Cross-Panel Sync
|
||||
</strong>
|
||||
<span className={styles.crossPanelSyncTooltipDescription}>
|
||||
Sync crosshair and tooltip across all the dashboard panels
|
||||
</span>
|
||||
<a
|
||||
href="https://signoz.io/docs/dashboards/interactivity/#cross-panel-sync"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.crossPanelSyncTooltipDocLink}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLink size={12} />
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
placement="top"
|
||||
mouseEnterDelay={0.5}
|
||||
>
|
||||
<SolidInfoCircle size="md" className={styles.crossPanelSyncInfoIcon} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className={styles.crossPanelSyncRow}>
|
||||
<div className={styles.crossPanelSyncInfo}>
|
||||
<Typography.Text className={styles.crossPanelSyncTitle}>
|
||||
Sync Mode
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.crossPanelSyncDescription}>
|
||||
Sync crosshair and tooltip across all the dashboard panels
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Radio.Group
|
||||
value={cursorSyncMode}
|
||||
onChange={(e): void => {
|
||||
setCursorSyncMode(e.target.value as DashboardCursorSync);
|
||||
}}
|
||||
>
|
||||
<Radio.Button value={DashboardCursorSync.None}>No Sync</Radio.Button>
|
||||
<Radio.Button value={DashboardCursorSync.Crosshair}>
|
||||
Crosshair
|
||||
</Radio.Button>
|
||||
<Radio.Button value={DashboardCursorSync.Tooltip}>Tooltip</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
{cursorSyncMode === DashboardCursorSync.Tooltip && (
|
||||
<div className={styles.crossPanelSyncRow}>
|
||||
<div className={styles.crossPanelSyncInfo}>
|
||||
<Typography.Text className={styles.crossPanelSyncTitle}>
|
||||
Synced Tooltip Series
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.crossPanelSyncDescription}>
|
||||
Show only series that intersect on group-by, or every series with the
|
||||
matching ones highlighted
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Radio.Group
|
||||
value={syncTooltipFilterMode}
|
||||
onChange={(e): void => {
|
||||
void logEvent(Events.TOOLTIP_SYNC_MODE_CHANGED, {
|
||||
path: getAbsoluteUrl(window.location.pathname),
|
||||
mode: e.target.value,
|
||||
});
|
||||
setSyncTooltipFilterMode(e.target.value as SyncTooltipFilterMode);
|
||||
}}
|
||||
>
|
||||
<Radio.Button value={SyncTooltipFilterMode.All}>All</Radio.Button>
|
||||
<Radio.Button value={SyncTooltipFilterMode.Filtered}>
|
||||
Filtered
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default CrossPanelSync;
|
||||
@@ -1,85 +0,0 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
// eslint-disable-next-line signoz/no-antd-components -- TODO: migrate Select/Input to @signozhq/ui
|
||||
import { Col, Input, Select, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddBadges';
|
||||
|
||||
import { Base64Icons } from '../utils';
|
||||
import styles from '../GeneralSettings.module.scss';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface GeneralFormProps {
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
tags: string[];
|
||||
onTitleChange: (value: string) => void;
|
||||
onDescriptionChange: (value: string) => void;
|
||||
onImageChange: (value: string) => void;
|
||||
onTagsChange: Dispatch<SetStateAction<string[]>>;
|
||||
}
|
||||
|
||||
function GeneralForm({
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
tags,
|
||||
onTitleChange,
|
||||
onDescriptionChange,
|
||||
onImageChange,
|
||||
onTagsChange,
|
||||
}: GeneralFormProps): JSX.Element {
|
||||
return (
|
||||
<Col className={styles.overviewSettings}>
|
||||
<Space direction="vertical" className={styles.formSpace}>
|
||||
<div>
|
||||
<Typography className={styles.dashboardName}>Dashboard Name</Typography>
|
||||
<section className={styles.nameIconInput}>
|
||||
<Select
|
||||
defaultActiveFirstOption
|
||||
data-testid="dashboard-image"
|
||||
suffixIcon={null}
|
||||
rootClassName={styles.dashboardImageInput}
|
||||
value={image}
|
||||
onChange={onImageChange}
|
||||
>
|
||||
{Base64Icons.map((icon) => (
|
||||
<Option value={icon} key={icon}>
|
||||
<img
|
||||
src={icon}
|
||||
alt="dashboard-icon"
|
||||
className={styles.listItemImage}
|
||||
/>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
data-testid="dashboard-name"
|
||||
className={styles.dashboardNameInput}
|
||||
value={title}
|
||||
onChange={(e): void => onTitleChange(e.target.value)}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Typography className={styles.dashboardName}>Description</Typography>
|
||||
<Input.TextArea
|
||||
data-testid="dashboard-desc"
|
||||
rows={6}
|
||||
value={description}
|
||||
className={styles.descriptionTextArea}
|
||||
onChange={(e): void => onDescriptionChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography className={styles.dashboardName}>Tags</Typography>
|
||||
<AddTags tags={tags} setTags={onTagsChange} />
|
||||
</div>
|
||||
</Space>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default GeneralForm;
|
||||
@@ -1,238 +0,0 @@
|
||||
.overviewContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.overviewSettings {
|
||||
padding: 16px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.crossPanelSyncGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.formSpace {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 21px;
|
||||
}
|
||||
|
||||
.crossPanelSyncSectionTitle {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.crossPanelSyncSectionHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.crossPanelSyncInfoIcon {
|
||||
cursor: help;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
.crossPanelSyncTooltipContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.crossPanelSyncTooltipTitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.crossPanelSyncTooltipDescription {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.crossPanelSyncTooltipDocLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--primary-background);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.crossPanelSyncRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
& + & {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
.crossPanelSyncInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.crossPanelSyncTitle {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.crossPanelSyncDescription {
|
||||
color: var(--l3-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.nameIconInput {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.dashboardImageInput {
|
||||
:global(.ant-select-selector) {
|
||||
display: flex;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 6px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
border: 1px solid var(--l1-border) !important;
|
||||
background: var(--l3-background) !important;
|
||||
|
||||
:global(.ant-select-selection-item) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&:global(.ant-select-dropdown) {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
:global(.ant-select-item) {
|
||||
padding: 0px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
:global(.ant-select-item-option-content) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listItemImage {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.dashboardNameInput {
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
.dashboardName {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.descriptionTextArea {
|
||||
padding: 6px 6px 6px 8px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
.overviewSettingsFooter {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: -webkit-fill-available;
|
||||
padding: 12px 16px 12px 0px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
height: 32px;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
.unsaved {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.unsavedDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50px;
|
||||
background: var(--primary-background);
|
||||
box-shadow: 0px 0px 6px 0px
|
||||
color-mix(in srgb, var(--primary-background) 40%, transparent);
|
||||
}
|
||||
|
||||
.unsavedChanges {
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.footerActionBtns {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.discardBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.saveBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0px !important;
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Check, X } from '@signozhq/icons';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import styles from '../GeneralSettings.module.scss';
|
||||
|
||||
interface UnsavedChangesFooterProps {
|
||||
count: number;
|
||||
isSaving: boolean;
|
||||
onDiscard: () => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
function UnsavedChangesFooter({
|
||||
count,
|
||||
isSaving,
|
||||
onDiscard,
|
||||
onSave,
|
||||
}: UnsavedChangesFooterProps): JSX.Element {
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
return (
|
||||
<div className={styles.overviewSettingsFooter}>
|
||||
<div className={styles.unsaved}>
|
||||
<div className={styles.unsavedDot} />
|
||||
<Typography.Text className={styles.unsavedChanges}>
|
||||
{count} unsaved change
|
||||
{count > 1 && 's'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className={styles.footerActionBtns}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={isSaving}
|
||||
prefix={<X size={14} />}
|
||||
onClick={onDiscard}
|
||||
className={styles.discardBtn}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
disabled={isSaving}
|
||||
loading={isSaving}
|
||||
prefix={<Check size={14} />}
|
||||
testId="save-dashboard-config"
|
||||
onClick={onSave}
|
||||
className={styles.saveBtn}
|
||||
>
|
||||
{t('save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UnsavedChangesFooter;
|
||||
@@ -1,170 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { patchDashboardV2 } from 'api/generated/services/dashboard';
|
||||
import type {
|
||||
DashboardtypesGettableDashboardV2DTO,
|
||||
DashboardtypesJSONPatchOperationDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { useDashboardStore } from '../../store/useDashboardStore';
|
||||
import CrossPanelSync from './CrossPanelSync/CrossPanelSync';
|
||||
import GeneralForm from './GeneralForm/GeneralForm';
|
||||
import UnsavedChangesFooter from './UnsavedChangesFooter/UnsavedChangesFooter';
|
||||
import { Base64Icons, stringsToTags, tagsToStrings } from './utils';
|
||||
import styles from './GeneralSettings.module.scss';
|
||||
|
||||
interface GeneralSettingsProps {
|
||||
dashboard: DashboardtypesGettableDashboardV2DTO;
|
||||
}
|
||||
|
||||
function GeneralSettings({ dashboard }: GeneralSettingsProps): JSX.Element {
|
||||
const id = dashboard.id;
|
||||
|
||||
const refetch = useDashboardStore((s) => s.refetch);
|
||||
|
||||
const title = dashboard.spec?.display?.name ?? '';
|
||||
const description = dashboard.spec?.display?.description ?? '';
|
||||
const image = dashboard.image || Base64Icons[0];
|
||||
const tagsAsStrings = useMemo(
|
||||
() => tagsToStrings(dashboard.tags ?? []),
|
||||
[dashboard.tags],
|
||||
);
|
||||
|
||||
const [updatedTitle, setUpdatedTitle] = useState<string>(title);
|
||||
const [updatedTags, setUpdatedTags] = useState<string[]>(tagsAsStrings);
|
||||
const [updatedDescription, setUpdatedDescription] =
|
||||
useState<string>(description);
|
||||
const [updatedImage, setUpdatedImage] = useState<string>(image);
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [numberOfUnsavedChanges, setNumberOfUnsavedChanges] =
|
||||
useState<number>(0);
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
// Sync state when dashboard refetches after a save
|
||||
useEffect(() => {
|
||||
setUpdatedTitle(title);
|
||||
setUpdatedDescription(description);
|
||||
setUpdatedImage(image);
|
||||
setUpdatedTags(tagsAsStrings);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dashboard.updatedAt]);
|
||||
|
||||
const buildPatch = useCallback((): DashboardtypesJSONPatchOperationDTO[] => {
|
||||
const ops: DashboardtypesJSONPatchOperationDTO[] = [];
|
||||
const replace = (
|
||||
path: string,
|
||||
value: unknown,
|
||||
): DashboardtypesJSONPatchOperationDTO => ({
|
||||
op: 'replace' as DashboardtypesJSONPatchOperationDTO['op'],
|
||||
path,
|
||||
value,
|
||||
});
|
||||
|
||||
if (updatedTitle !== title) {
|
||||
ops.push(replace('/spec/display/name', updatedTitle));
|
||||
}
|
||||
if (updatedDescription !== description) {
|
||||
ops.push(replace('/spec/display/description', updatedDescription));
|
||||
}
|
||||
if (updatedImage !== image) {
|
||||
ops.push(replace('/image', updatedImage));
|
||||
}
|
||||
if (!isEqual(updatedTags, tagsAsStrings)) {
|
||||
ops.push(replace('/tags', stringsToTags(updatedTags)));
|
||||
}
|
||||
return ops;
|
||||
}, [
|
||||
updatedTitle,
|
||||
title,
|
||||
updatedDescription,
|
||||
description,
|
||||
updatedImage,
|
||||
image,
|
||||
updatedTags,
|
||||
tagsAsStrings,
|
||||
]);
|
||||
|
||||
const onSaveHandler = useCallback(async (): Promise<void> => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const ops = buildPatch();
|
||||
if (ops.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
await patchDashboardV2({ id }, ops);
|
||||
toast.success('Dashboard updated');
|
||||
refetch();
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [id, buildPatch, refetch, showErrorModal]);
|
||||
|
||||
useEffect(() => {
|
||||
let n = 0;
|
||||
const initialValues = [title, description, tagsAsStrings, image];
|
||||
const updatedValues = [
|
||||
updatedTitle,
|
||||
updatedDescription,
|
||||
updatedTags,
|
||||
updatedImage,
|
||||
];
|
||||
initialValues.forEach((val, index) => {
|
||||
if (!isEqual(val, updatedValues[index])) {
|
||||
n += 1;
|
||||
}
|
||||
});
|
||||
setNumberOfUnsavedChanges(n);
|
||||
}, [
|
||||
description,
|
||||
image,
|
||||
tagsAsStrings,
|
||||
title,
|
||||
updatedDescription,
|
||||
updatedImage,
|
||||
updatedTags,
|
||||
updatedTitle,
|
||||
]);
|
||||
|
||||
const discardHandler = useCallback((): void => {
|
||||
setUpdatedTitle(title);
|
||||
setUpdatedImage(image);
|
||||
setUpdatedTags(tagsAsStrings);
|
||||
setUpdatedDescription(description);
|
||||
}, [title, image, tagsAsStrings, description]);
|
||||
|
||||
return (
|
||||
<div className={styles.overviewContent}>
|
||||
<GeneralForm
|
||||
title={updatedTitle}
|
||||
description={updatedDescription}
|
||||
image={updatedImage}
|
||||
tags={updatedTags}
|
||||
onTitleChange={setUpdatedTitle}
|
||||
onDescriptionChange={setUpdatedDescription}
|
||||
onImageChange={setUpdatedImage}
|
||||
onTagsChange={setUpdatedTags}
|
||||
/>
|
||||
<CrossPanelSync dashboardId={id} />
|
||||
{numberOfUnsavedChanges > 0 && (
|
||||
<UnsavedChangesFooter
|
||||
count={numberOfUnsavedChanges}
|
||||
isSaving={isSaving}
|
||||
onDiscard={discardHandler}
|
||||
onSave={onSaveHandler}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GeneralSettings;
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { TagtypesPostableTagDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
|
||||
|
||||
// tag UX, a string with no ':' is round-tripped as `{key: x, value: x}` and
|
||||
// collapsed back to just `x` for display.
|
||||
export function tagsToStrings(tags: TagtypesPostableTagDTO[]): string[] {
|
||||
return tags.map((t) => (t.key === t.value ? t.key : `${t.key}:${t.value}`));
|
||||
}
|
||||
|
||||
export function stringsToTags(tagStrings: string[]): TagtypesPostableTagDTO[] {
|
||||
return tagStrings
|
||||
.map((s) => {
|
||||
const trimmed = s.trim();
|
||||
const idx = trimmed.indexOf(':');
|
||||
if (idx === -1) {
|
||||
return { key: trimmed, value: trimmed };
|
||||
}
|
||||
const key = trimmed.slice(0, idx).trim();
|
||||
const value = trimmed.slice(idx + 1).trim();
|
||||
return { key, value: value || key };
|
||||
})
|
||||
.filter((t) => t.key.length > 0);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Braces, Globe, Table } from '@signozhq/icons';
|
||||
import { Tabs } from '@signozhq/ui/tabs';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import GeneralSettings from './General';
|
||||
import { SettingsTabPlaceholder } from './utils';
|
||||
|
||||
import styles from './DashboardSettings.module.scss';
|
||||
|
||||
interface DashboardSettingsProps {
|
||||
dashboard: DashboardtypesGettableDashboardV2DTO;
|
||||
}
|
||||
|
||||
function tabLabel(icon: JSX.Element, text: string): JSX.Element {
|
||||
return (
|
||||
<span className={styles.tabLabel}>
|
||||
{icon}
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardSettings({ dashboard }: DashboardSettingsProps): JSX.Element {
|
||||
const items = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'general',
|
||||
label: tabLabel(<Table size={14} />, 'General'),
|
||||
children: <GeneralSettings dashboard={dashboard} />,
|
||||
},
|
||||
{
|
||||
key: 'variables',
|
||||
label: tabLabel(<Braces size={14} />, 'Variables'),
|
||||
children: (
|
||||
<SettingsTabPlaceholder message="V2 dashboard variables coming next." />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'public-dashboard',
|
||||
label: tabLabel(<Globe size={14} />, 'Publish'),
|
||||
children: (
|
||||
<SettingsTabPlaceholder message="V2 public dashboard publishing coming next." />
|
||||
),
|
||||
},
|
||||
],
|
||||
[dashboard],
|
||||
);
|
||||
|
||||
return <Tabs defaultValue="general" items={items} />;
|
||||
}
|
||||
|
||||
export default DashboardSettings;
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Empty } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import styles from './DashboardSettings.module.scss';
|
||||
|
||||
/**
|
||||
* TEMPORARY: stand-in for the not-yet-built Variables / Publish settings tabs.
|
||||
* Will be cleaned up later once those tabs ship their real content.
|
||||
*/
|
||||
export function SettingsTabPlaceholder({
|
||||
message,
|
||||
}: {
|
||||
message: string;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className={styles.placeholder}>
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={<Typography.Text>{message}</Typography.Text>}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
.emptyState {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 48px 16px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.emoji {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.welcome {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.08px;
|
||||
}
|
||||
|
||||
.welcomeInfo {
|
||||
color: var(--l3-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.addPanel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border: 1px dashed var(--l1-border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.addPanelText {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
|
||||
.icon {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.addPanelCopy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.addPanelTitle {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.addPanelInfo {
|
||||
color: var(--l3-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||
|
||||
import dashboardEmojiUrl from '@/assets/Icons/dashboard_emoji.svg';
|
||||
import landscapeUrl from '@/assets/Icons/landscape.svg';
|
||||
|
||||
import styles from './DashboardEmptyState.module.scss';
|
||||
|
||||
interface DashboardEmptyStateProps {
|
||||
canAddPanel: boolean;
|
||||
}
|
||||
|
||||
function DashboardEmptyState({
|
||||
canAddPanel,
|
||||
}: DashboardEmptyStateProps): JSX.Element {
|
||||
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
|
||||
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||
);
|
||||
|
||||
return (
|
||||
<section className={styles.emptyState}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.heading}>
|
||||
<img src={dashboardEmojiUrl} alt="" className={styles.emoji} />
|
||||
<Typography.Text className={styles.welcome}>
|
||||
Welcome to your new dashboard
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.welcomeInfo}>
|
||||
Follow the steps to populate it with data and share with your teammates
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className={styles.addPanel}>
|
||||
<div className={styles.addPanelText}>
|
||||
<img src={landscapeUrl} alt="" className={styles.icon} />
|
||||
<div className={styles.addPanelCopy}>
|
||||
<Typography.Text className={styles.addPanelTitle}>
|
||||
Add panels
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.addPanelInfo}>
|
||||
Add panels to visualize your data
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
{canAddPanel && (
|
||||
<Button
|
||||
color="primary"
|
||||
prefix={<Plus size="md" />}
|
||||
onClick={(): void => setIsPanelTypeSelectionModalOpen(true)}
|
||||
testId="add-panel"
|
||||
>
|
||||
New Panel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardEmptyState;
|
||||
@@ -4,7 +4,7 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: var(--bg-ink-400, #0b0c0e);
|
||||
border: 1px solid var(--l1-border);
|
||||
border: 1px solid var(--bg-slate-400, #1d212d);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
border-bottom: 1px solid var(--bg-slate-400, #1d212d);
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--bg-vanilla-400, #8993ae);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -12,15 +12,7 @@ import type { MovePanelArgs } from './hooks/useMovePanelToSection';
|
||||
import PanelActionsMenu from './PanelActionsMenu/PanelActionsMenu';
|
||||
import styles from './Panel.module.scss';
|
||||
|
||||
/** Panel action context — present together only in editable sectioned mode. */
|
||||
export interface PanelActionsConfig {
|
||||
currentLayoutIndex: number;
|
||||
sections: DashboardSection[];
|
||||
onMovePanel: (args: MovePanelArgs) => void;
|
||||
onDeletePanel: (args: DeletePanelArgs) => void;
|
||||
}
|
||||
|
||||
interface PanelProps {
|
||||
interface Props {
|
||||
panel: DashboardtypesPanelDTO | undefined;
|
||||
panelId: string;
|
||||
/**
|
||||
@@ -29,16 +21,22 @@ interface PanelProps {
|
||||
* data. Currently unused on purpose.
|
||||
*/
|
||||
isVisible?: boolean;
|
||||
/** Move/delete actions — present only in editable sectioned mode. */
|
||||
panelActions?: PanelActionsConfig;
|
||||
/** Section actions — present only in editable sectioned mode. */
|
||||
currentLayoutIndex?: number;
|
||||
sections?: DashboardSection[];
|
||||
onMovePanel?: (args: MovePanelArgs) => void;
|
||||
onDeletePanel?: (args: DeletePanelArgs) => void;
|
||||
}
|
||||
|
||||
function Panel({
|
||||
panel,
|
||||
panelId,
|
||||
isVisible,
|
||||
panelActions,
|
||||
}: PanelProps): JSX.Element {
|
||||
currentLayoutIndex,
|
||||
sections,
|
||||
onMovePanel,
|
||||
onDeletePanel,
|
||||
}: Props): JSX.Element {
|
||||
const name = panel?.spec?.display?.name || `Panel ${panelId.slice(0, 6)}`;
|
||||
const description = panel?.spec?.display?.description;
|
||||
const kind = panel?.spec?.plugin?.kind?.replace(/^signoz\//, '') ?? 'unknown';
|
||||
@@ -67,13 +65,13 @@ function Panel({
|
||||
</Typography.Text>
|
||||
<Badge className={styles.badge}>{kind}</Badge>
|
||||
</div>
|
||||
{panelActions ? (
|
||||
{currentLayoutIndex !== undefined && (onMovePanel || onDeletePanel) ? (
|
||||
<PanelActionsMenu
|
||||
panelId={panelId}
|
||||
currentLayoutIndex={panelActions.currentLayoutIndex}
|
||||
sections={panelActions.sections}
|
||||
onMovePanel={panelActions.onMovePanel}
|
||||
onDeletePanel={panelActions.onDeletePanel}
|
||||
currentLayoutIndex={currentLayoutIndex}
|
||||
sections={sections ?? []}
|
||||
onMovePanel={onMovePanel}
|
||||
onDeletePanel={onDeletePanel}
|
||||
/>
|
||||
) : (
|
||||
<EllipsisVertical size={14} />
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--bg-vanilla-400, #8993ae);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l2-background);
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { EllipsisVertical, FolderInput, Trash2 } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||
import type { MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
|
||||
@@ -9,7 +8,7 @@ import type { DeletePanelArgs } from '../hooks/useDeletePanel';
|
||||
import type { MovePanelArgs } from '../hooks/useMovePanelToSection';
|
||||
import styles from './PanelActionsMenu.module.scss';
|
||||
|
||||
interface PanelActionsMenuProps {
|
||||
interface Props {
|
||||
panelId: string;
|
||||
currentLayoutIndex: number;
|
||||
sections: DashboardSection[];
|
||||
@@ -23,7 +22,7 @@ function PanelActionsMenu({
|
||||
sections,
|
||||
onMovePanel,
|
||||
onDeletePanel,
|
||||
}: PanelActionsMenuProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const items = useMemo<MenuItem[]>(() => {
|
||||
const result: MenuItem[] = [];
|
||||
|
||||
@@ -76,11 +75,8 @@ function PanelActionsMenu({
|
||||
|
||||
return (
|
||||
<DropdownMenuSimple menu={{ items }}>
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={styles.trigger}
|
||||
aria-label="Panel actions"
|
||||
data-testid={`panel-actions-${panelId}`}
|
||||
@@ -91,7 +87,7 @@ function PanelActionsMenu({
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
>
|
||||
<EllipsisVertical size={14} />
|
||||
</Button>
|
||||
</button>
|
||||
</DropdownMenuSimple>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: var(--bg-ink-400, #0b0c0e);
|
||||
border: 1px solid var(--l1-border);
|
||||
border: 1px solid var(--bg-slate-400, #1d212d);
|
||||
border-radius: 4px;
|
||||
color: var(--l1-foreground);
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
||||
|
||||
@@ -1,10 +1,48 @@
|
||||
import { Modal } from 'antd';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import {
|
||||
BarChart,
|
||||
ChartLine,
|
||||
ChartPie,
|
||||
Hash,
|
||||
List,
|
||||
Table,
|
||||
} from '@signozhq/icons';
|
||||
|
||||
import { PANEL_TYPES } from './constants';
|
||||
import styles from './PanelTypeSelectionModal.module.scss';
|
||||
|
||||
interface PanelTypeSelectionModalProps {
|
||||
interface PanelType {
|
||||
pluginKind: string;
|
||||
label: string;
|
||||
icon: JSX.Element;
|
||||
}
|
||||
|
||||
const PANEL_TYPES: PanelType[] = [
|
||||
{
|
||||
pluginKind: 'signoz/TimeSeriesPanel',
|
||||
label: 'Time Series',
|
||||
icon: <ChartLine size={16} />,
|
||||
},
|
||||
{ pluginKind: 'signoz/NumberPanel', label: 'Value', icon: <Hash size={16} /> },
|
||||
{ pluginKind: 'signoz/TablePanel', label: 'Table', icon: <Table size={16} /> },
|
||||
{
|
||||
pluginKind: 'signoz/BarChartPanel',
|
||||
label: 'Bar Chart',
|
||||
icon: <BarChart size={16} />,
|
||||
},
|
||||
{
|
||||
pluginKind: 'signoz/PieChartPanel',
|
||||
label: 'Pie Chart',
|
||||
icon: <ChartPie size={16} />,
|
||||
},
|
||||
{
|
||||
pluginKind: 'signoz/HistogramPanel',
|
||||
label: 'Histogram',
|
||||
icon: <BarChart size={16} />,
|
||||
},
|
||||
{ pluginKind: 'signoz/ListPanel', label: 'List', icon: <List size={16} /> },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSelect: (pluginKind: string) => void;
|
||||
@@ -14,7 +52,7 @@ function PanelTypeSelectionModal({
|
||||
open,
|
||||
onClose,
|
||||
onSelect,
|
||||
}: PanelTypeSelectionModalProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
@@ -25,17 +63,16 @@ function PanelTypeSelectionModal({
|
||||
>
|
||||
<div className={styles.grid}>
|
||||
{PANEL_TYPES.map((type) => (
|
||||
<Button
|
||||
<button
|
||||
key={type.pluginKind}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className={styles.typeButton}
|
||||
data-testid={`panel-type-${type.pluginKind}`}
|
||||
onClick={(): void => onSelect(type.pluginKind)}
|
||||
>
|
||||
{type.icon}
|
||||
{type.label}
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import {
|
||||
BarChart,
|
||||
ChartLine,
|
||||
ChartPie,
|
||||
Hash,
|
||||
List,
|
||||
Table,
|
||||
} from '@signozhq/icons';
|
||||
|
||||
import type { PanelType } from './types';
|
||||
|
||||
export const PANEL_TYPES: PanelType[] = [
|
||||
{
|
||||
pluginKind: 'signoz/TimeSeriesPanel',
|
||||
label: 'Time Series',
|
||||
icon: <ChartLine size={16} />,
|
||||
},
|
||||
{ pluginKind: 'signoz/NumberPanel', label: 'Value', icon: <Hash size={16} /> },
|
||||
{ pluginKind: 'signoz/TablePanel', label: 'Table', icon: <Table size={16} /> },
|
||||
{
|
||||
pluginKind: 'signoz/BarChartPanel',
|
||||
label: 'Bar Chart',
|
||||
icon: <BarChart size={16} />,
|
||||
},
|
||||
{
|
||||
pluginKind: 'signoz/PieChartPanel',
|
||||
label: 'Pie Chart',
|
||||
icon: <ChartPie size={16} />,
|
||||
},
|
||||
{
|
||||
pluginKind: 'signoz/HistogramPanel',
|
||||
label: 'Histogram',
|
||||
icon: <BarChart size={16} />,
|
||||
},
|
||||
{ pluginKind: 'signoz/ListPanel', label: 'List', icon: <List size={16} /> },
|
||||
];
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface PanelType {
|
||||
pluginKind: string;
|
||||
label: string;
|
||||
icon: JSX.Element;
|
||||
}
|
||||
@@ -36,6 +36,9 @@ export function useAddPanelToSection({
|
||||
|
||||
return useCallback(
|
||||
async ({ layoutIndex, pluginKind }: AddPanelArgs): Promise<void> => {
|
||||
if (!dashboardId) {
|
||||
return;
|
||||
}
|
||||
const target = sections.find((s) => s.layoutIndex === layoutIndex);
|
||||
if (!target) {
|
||||
return;
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--l1-border);
|
||||
border: 1px dashed var(--bg-slate-400, #1d212d);
|
||||
border-radius: 4px;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--bg-vanilla-400, #8993ae);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-robin-500);
|
||||
color: var(--l1-foreground);
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import type { DashboardtypesLayoutDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import type { DashboardSection } from '../../../utils';
|
||||
@@ -11,7 +10,7 @@ import styles from './AddSectionControl.module.scss';
|
||||
|
||||
const DEFAULT_SECTION_TITLE = 'New section';
|
||||
|
||||
interface AddSectionControlProps {
|
||||
interface Props {
|
||||
sections: DashboardSection[];
|
||||
layouts: DashboardtypesLayoutDTO[] | undefined | null;
|
||||
isSectioned: boolean;
|
||||
@@ -21,7 +20,7 @@ function AddSectionControl({
|
||||
sections,
|
||||
layouts,
|
||||
isSectioned,
|
||||
}: AddSectionControlProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const [isMigrationOpen, setIsMigrationOpen] = useState(false);
|
||||
const { addSection } = useAddSection({ layouts });
|
||||
const { migrate, isSaving } = useFirstSectionMigration({ sections });
|
||||
@@ -31,31 +30,30 @@ function AddSectionControl({
|
||||
const needsMigration =
|
||||
!isSectioned && sections.some((s) => s.items.length > 0);
|
||||
|
||||
const handleClick = useCallback((): void => {
|
||||
const handleClick = (): void => {
|
||||
if (needsMigration) {
|
||||
setIsMigrationOpen(true);
|
||||
return;
|
||||
}
|
||||
void addSection(DEFAULT_SECTION_TITLE);
|
||||
}, [needsMigration, addSection]);
|
||||
};
|
||||
|
||||
const handleConfirmMigration = useCallback(async (): Promise<void> => {
|
||||
const handleConfirmMigration = async (): Promise<void> => {
|
||||
await migrate(DEFAULT_SECTION_TITLE);
|
||||
setIsMigrationOpen(false);
|
||||
}, [migrate]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className={styles.addButton}
|
||||
onClick={handleClick}
|
||||
data-testid="add-section"
|
||||
>
|
||||
<Plus size={14} />
|
||||
Add section
|
||||
</Button>
|
||||
</button>
|
||||
<FirstSectionMigrationModal
|
||||
open={isMigrationOpen}
|
||||
isSaving={isSaving}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Modal } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
interface FirstSectionMigrationModalProps {
|
||||
interface Props {
|
||||
open: boolean;
|
||||
isSaving: boolean;
|
||||
onClose: () => void;
|
||||
@@ -18,7 +18,7 @@ function FirstSectionMigrationModal({
|
||||
isSaving,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: FirstSectionMigrationModalProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { Modal } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
|
||||
interface RenameSectionModalProps {
|
||||
interface Props {
|
||||
open: boolean;
|
||||
initialValue: string;
|
||||
isSaving: boolean;
|
||||
@@ -16,7 +16,7 @@ function RenameSectionModal({
|
||||
isSaving,
|
||||
onClose,
|
||||
onSubmit,
|
||||
}: RenameSectionModalProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const [value, setValue] = useState<string>(initialValue);
|
||||
|
||||
// Reseed the field each time the modal opens.
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
.section {
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dragging {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.deleteModal :global(.ant-modal-confirm-body) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.emptySection {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 24px 12px;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Modal } from 'antd';
|
||||
|
||||
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
||||
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||
|
||||
import ConfirmDeleteDialog from '../../../components/ConfirmDeleteDialog/ConfirmDeleteDialog';
|
||||
import type { DashboardSection } from '../../../utils';
|
||||
import type { AddPanelArgs } from '../../Panel/hooks/useAddPanelToSection';
|
||||
import type { DeletePanelArgs } from '../../Panel/hooks/useDeletePanel';
|
||||
@@ -22,7 +19,7 @@ import SectionHeader, {
|
||||
} from '../SectionHeader/SectionHeader';
|
||||
import styles from './Section.module.scss';
|
||||
|
||||
interface SectionProps {
|
||||
interface Props {
|
||||
section: DashboardSection;
|
||||
/** Adds a panel to this section; present only in editable sectioned mode. */
|
||||
onAddPanel?: (args: AddPanelArgs) => void;
|
||||
@@ -41,12 +38,8 @@ function Section({
|
||||
onMovePanel,
|
||||
onDeletePanel,
|
||||
dragHandle,
|
||||
}: SectionProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const isEditable = useDashboardStore((s) => s.isEditable);
|
||||
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
|
||||
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||
);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
// Placeholder signal for lazy panel query-loading (consumed in a later PR):
|
||||
// true once the section scrolls into (or near) the viewport.
|
||||
@@ -61,30 +54,30 @@ function Section({
|
||||
layoutIndex: section.layoutIndex,
|
||||
});
|
||||
|
||||
const handleRenameSubmit = useCallback(
|
||||
async (title: string): Promise<void> => {
|
||||
const ok = await rename(title);
|
||||
if (ok) {
|
||||
setIsRenaming(false);
|
||||
}
|
||||
},
|
||||
[rename],
|
||||
);
|
||||
const handleRenameSubmit = async (title: string): Promise<void> => {
|
||||
const ok = await rename(title);
|
||||
if (ok) {
|
||||
setIsRenaming(false);
|
||||
}
|
||||
};
|
||||
|
||||
const [isAddingPanel, setIsAddingPanel] = useState(false);
|
||||
const handleSelectPanelType = useCallback(
|
||||
(pluginKind: string): void => {
|
||||
onAddPanel?.({ layoutIndex: section.layoutIndex, pluginKind });
|
||||
setIsAddingPanel(false);
|
||||
},
|
||||
[onAddPanel, section.layoutIndex],
|
||||
);
|
||||
const handleSelectPanelType = (pluginKind: string): void => {
|
||||
onAddPanel?.({ layoutIndex: section.layoutIndex, pluginKind });
|
||||
setIsAddingPanel(false);
|
||||
};
|
||||
|
||||
const { deleteSection } = useDeleteSection({ section });
|
||||
const handleDeleteSection = useCallback((): void => {
|
||||
void deleteSection();
|
||||
setIsDeleteOpen(false);
|
||||
}, [deleteSection]);
|
||||
const confirmDeleteSection = (): void => {
|
||||
Modal.confirm({
|
||||
title: `Delete section "${section.title ?? ''}"?`,
|
||||
content: 'Panels in this section will be removed.',
|
||||
okText: 'Delete',
|
||||
okButtonProps: { danger: true },
|
||||
centered: true,
|
||||
onOk: () => deleteSection(),
|
||||
});
|
||||
};
|
||||
|
||||
const grid = (
|
||||
<SectionGrid
|
||||
@@ -125,35 +118,13 @@ function Section({
|
||||
onToggle={toggle}
|
||||
repeatVariable={section.repeatVariable}
|
||||
dragHandle={dragHandle}
|
||||
actions={
|
||||
isEditable
|
||||
? {
|
||||
onRename: (): void => setIsRenaming(true),
|
||||
onAddPanel: (): void => setIsAddingPanel(true),
|
||||
onDeleteSection: (): void => setIsDeleteOpen(true),
|
||||
}
|
||||
: undefined
|
||||
onRename={isEditable ? (): void => setIsRenaming(true) : undefined}
|
||||
onAddPanel={
|
||||
isEditable && onAddPanel ? (): void => setIsAddingPanel(true) : undefined
|
||||
}
|
||||
onDeleteSection={isEditable ? confirmDeleteSection : undefined}
|
||||
/>
|
||||
{open &&
|
||||
(section.items.length > 0 ? (
|
||||
grid
|
||||
) : (
|
||||
<div className={styles.emptySection}>
|
||||
{isEditable && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="dashed"
|
||||
color="secondary"
|
||||
prefix={<Plus size="md" />}
|
||||
onClick={(): void => setIsPanelTypeSelectionModalOpen(true)}
|
||||
testId={`section-add-panel-${section.id}`}
|
||||
>
|
||||
New Panel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{open ? grid : null}
|
||||
<RenameSectionModal
|
||||
open={isRenaming}
|
||||
initialValue={section.title}
|
||||
@@ -166,13 +137,6 @@ function Section({
|
||||
onClose={(): void => setIsAddingPanel(false)}
|
||||
onSelect={handleSelectPanelType}
|
||||
/>
|
||||
<ConfirmDeleteDialog
|
||||
open={isDeleteOpen}
|
||||
title={`Delete section "${section.title ?? ''}"?`}
|
||||
description="Panels in this section will be removed."
|
||||
onConfirm={handleDeleteSection}
|
||||
onClose={(): void => setIsDeleteOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--bg-vanilla-400, #8993ae);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l2-background);
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useMemo } from 'react';
|
||||
import { EllipsisVertical, PenLine, Plus, Trash2 } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||
import type { MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
|
||||
import styles from './SectionActionsMenu.module.scss';
|
||||
|
||||
interface SectionActionsMenuProps {
|
||||
interface Props {
|
||||
sectionId: string;
|
||||
onAddPanel?: () => void;
|
||||
onRename?: () => void;
|
||||
@@ -18,7 +17,7 @@ function SectionActionsMenu({
|
||||
onAddPanel,
|
||||
onRename,
|
||||
onDeleteSection,
|
||||
}: SectionActionsMenuProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const items = useMemo<MenuItem[]>(() => {
|
||||
const result: MenuItem[] = [];
|
||||
if (onAddPanel) {
|
||||
@@ -54,17 +53,14 @@ function SectionActionsMenu({
|
||||
|
||||
return (
|
||||
<DropdownMenuSimple menu={{ items }}>
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={styles.trigger}
|
||||
aria-label="Section actions"
|
||||
data-testid={`dashboard-section-actions-${sectionId}`}
|
||||
>
|
||||
<EllipsisVertical size={14} />
|
||||
</Button>
|
||||
</button>
|
||||
</DropdownMenuSimple>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { DashboardSection } from '../../../utils';
|
||||
import SectionHeader from '../SectionHeader/SectionHeader';
|
||||
import styles from './SectionDragPreview.module.scss';
|
||||
|
||||
interface SectionDragPreviewProps {
|
||||
interface Props {
|
||||
section: DashboardSection;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ interface SectionDragPreviewProps {
|
||||
* dragged. Deliberately header-only (no react-grid-layout) so the overlay is
|
||||
* cheap and never triggers RGL width re-measurement.
|
||||
*/
|
||||
function SectionDragPreview({ section }: SectionDragPreviewProps): JSX.Element {
|
||||
function SectionDragPreview({ section }: Props): JSX.Element {
|
||||
const panelCount = section.items.length;
|
||||
const title = `${section.title ?? ''} · ${panelCount} ${
|
||||
panelCount === 1 ? 'panel' : 'panels'
|
||||
|
||||
@@ -11,7 +11,7 @@ import styles from './SectionGrid.module.scss';
|
||||
|
||||
const ResponsiveGridLayout = WidthProvider(GridLayout);
|
||||
|
||||
interface SectionGridProps {
|
||||
interface Props {
|
||||
items: DashboardSection['items'];
|
||||
layoutIndex: number;
|
||||
/** Forwarded to panels — true when the parent section is in the viewport. */
|
||||
@@ -29,7 +29,7 @@ function SectionGrid({
|
||||
sections,
|
||||
onMovePanel,
|
||||
onDeletePanel,
|
||||
}: SectionGridProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const isEditable = useDashboardStore((s) => s.isEditable);
|
||||
const rglLayout = useMemo<Layout[]>(
|
||||
() =>
|
||||
@@ -66,16 +66,10 @@ function SectionGrid({
|
||||
panel={item.panel}
|
||||
panelId={item.id}
|
||||
isVisible={isVisible}
|
||||
panelActions={
|
||||
isEditable && onMovePanel && onDeletePanel
|
||||
? {
|
||||
currentLayoutIndex: layoutIndex,
|
||||
sections: sections ?? [],
|
||||
onMovePanel,
|
||||
onDeletePanel,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
currentLayoutIndex={layoutIndex}
|
||||
sections={isEditable ? sections : undefined}
|
||||
onMovePanel={isEditable ? onMovePanel : undefined}
|
||||
onDeletePanel={isEditable ? onDeletePanel : undefined}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
padding: 8px 12px;
|
||||
|
||||
&.headerOpen {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--bg-vanilla-400, #8993ae);
|
||||
cursor: grab;
|
||||
|
||||
&:active {
|
||||
@@ -33,8 +33,7 @@
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
// Muted chevron; the title below carries the prominent heading color.
|
||||
color: var(--l2-foreground);
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
min-width: 0;
|
||||
@@ -42,8 +41,6 @@
|
||||
|
||||
.title {
|
||||
margin-left: 4px;
|
||||
color: var(--l1-foreground);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { DraggableAttributes } from '@dnd-kit/core';
|
||||
import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
|
||||
import { ChevronDown, ChevronRight, GripVertical } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
|
||||
@@ -14,14 +13,7 @@ export interface SectionDragHandle {
|
||||
setActivatorNodeRef: (element: HTMLElement | null) => void;
|
||||
}
|
||||
|
||||
/** Editable-mode section actions — present together or not at all. */
|
||||
export interface SectionHeaderActions {
|
||||
onRename: () => void;
|
||||
onAddPanel: () => void;
|
||||
onDeleteSection: () => void;
|
||||
}
|
||||
|
||||
interface SectionHeaderProps {
|
||||
interface Props {
|
||||
sectionId: string;
|
||||
title: string;
|
||||
open: boolean;
|
||||
@@ -29,8 +21,9 @@ interface SectionHeaderProps {
|
||||
repeatVariable?: string;
|
||||
/** Provided by SortableSection in sectioned mode; absent for untitled/free-flow. */
|
||||
dragHandle?: SectionDragHandle;
|
||||
/** Present only in editable mode; absent (read-only) when locked/no-permission. */
|
||||
actions?: SectionHeaderActions;
|
||||
onRename?: () => void;
|
||||
onAddPanel?: () => void;
|
||||
onDeleteSection?: () => void;
|
||||
}
|
||||
|
||||
function SectionHeader({
|
||||
@@ -40,16 +33,16 @@ function SectionHeader({
|
||||
onToggle,
|
||||
repeatVariable,
|
||||
dragHandle,
|
||||
actions,
|
||||
}: SectionHeaderProps): JSX.Element {
|
||||
onRename,
|
||||
onAddPanel,
|
||||
onDeleteSection,
|
||||
}: Props): JSX.Element {
|
||||
const hasActions = !!(onAddPanel || onRename || onDeleteSection);
|
||||
return (
|
||||
<div className={cx(styles.header, { [styles.headerOpen]: open })}>
|
||||
{dragHandle ? (
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={styles.dragHandle}
|
||||
ref={dragHandle.setActivatorNodeRef}
|
||||
aria-label="Drag to reorder section"
|
||||
@@ -58,12 +51,10 @@ function SectionHeader({
|
||||
{...dragHandle.listeners}
|
||||
>
|
||||
<GripVertical size={14} />
|
||||
</Button>
|
||||
</button>
|
||||
) : null}
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className={styles.toggle}
|
||||
onClick={onToggle}
|
||||
data-testid={`dashboard-section-toggle-${sectionId}`}
|
||||
@@ -75,13 +66,13 @@ function SectionHeader({
|
||||
(repeats per ${repeatVariable})
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</Button>
|
||||
{actions ? (
|
||||
</button>
|
||||
{hasActions ? (
|
||||
<SectionActionsMenu
|
||||
sectionId={sectionId}
|
||||
onAddPanel={actions.onAddPanel}
|
||||
onRename={actions.onRename}
|
||||
onDeleteSection={actions.onDeleteSection}
|
||||
onAddPanel={onAddPanel}
|
||||
onRename={onRename}
|
||||
onDeleteSection={onDeleteSection}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -20,12 +20,12 @@ import Section from './Section/Section';
|
||||
import SectionDragPreview from './SectionDragPreview/SectionDragPreview';
|
||||
import SortableSection from './SortableSection';
|
||||
|
||||
interface SectionListProps {
|
||||
interface Props {
|
||||
sections: DashboardSection[];
|
||||
layouts: DashboardtypesLayoutDTO[] | undefined | null;
|
||||
}
|
||||
|
||||
function SectionList({ sections, layouts }: SectionListProps): JSX.Element {
|
||||
function SectionList({ sections, layouts }: Props): JSX.Element {
|
||||
const isEditable = useDashboardStore((s) => s.isEditable);
|
||||
|
||||
const {
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { DeletePanelArgs } from '../Panel/hooks/useDeletePanel';
|
||||
import type { MovePanelArgs } from '../Panel/hooks/useMovePanelToSection';
|
||||
import Section from './Section/Section';
|
||||
|
||||
interface SortableSectionProps {
|
||||
interface Props {
|
||||
section: DashboardSection;
|
||||
sections: DashboardSection[];
|
||||
onAddPanel: (args: AddPanelArgs) => void;
|
||||
@@ -21,7 +21,7 @@ function SortableSection({
|
||||
onAddPanel,
|
||||
onMovePanel,
|
||||
onDeletePanel,
|
||||
}: SortableSectionProps): JSX.Element {
|
||||
}: Props): JSX.Element {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
|
||||
import { Empty } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type {
|
||||
DashboardtypesLayoutDTO,
|
||||
DashboardtypesPanelDTO,
|
||||
@@ -7,7 +9,7 @@ import type {
|
||||
|
||||
import { useDashboardStore } from '../store/useDashboardStore';
|
||||
import { layoutsToSections } from '../utils';
|
||||
import DashboardEmptyState from './DashboardEmptyState/DashboardEmptyState';
|
||||
import AddSectionControl from './Section/AddSectionControl/AddSectionControl';
|
||||
import Section from './Section/Section/Section';
|
||||
import SectionList from './Section/SectionList';
|
||||
import styles from './PanelsAndSectionsLayout.module.scss';
|
||||
@@ -15,15 +17,12 @@ import styles from './PanelsAndSectionsLayout.module.scss';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
|
||||
interface PanelsAndSectionsLayoutProps {
|
||||
interface Props {
|
||||
layouts: DashboardtypesLayoutDTO[];
|
||||
panels: Record<string, DashboardtypesPanelDTO | undefined>;
|
||||
}
|
||||
|
||||
function PanelsAndSectionsLayout({
|
||||
layouts,
|
||||
panels,
|
||||
}: PanelsAndSectionsLayoutProps): JSX.Element {
|
||||
function PanelsAndSectionsLayout({ layouts, panels }: Props): JSX.Element {
|
||||
const isEditable = useDashboardStore((s) => s.isEditable);
|
||||
|
||||
const sections = useMemo(
|
||||
@@ -41,7 +40,16 @@ function PanelsAndSectionsLayout({
|
||||
|
||||
const renderContent = (): ReactNode => {
|
||||
if (isEmpty) {
|
||||
return <DashboardEmptyState canAddPanel={isEditable} />;
|
||||
return (
|
||||
<div className={styles.emptyState}>
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={
|
||||
<Typography.Text>No panels in this dashboard yet</Typography.Text>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSectioned) {
|
||||
@@ -53,7 +61,18 @@ function PanelsAndSectionsLayout({
|
||||
));
|
||||
};
|
||||
|
||||
return <div className={styles.body}>{renderContent()}</div>;
|
||||
return (
|
||||
<div className={styles.body}>
|
||||
{renderContent()}
|
||||
{isEditable ? (
|
||||
<AddSectionControl
|
||||
sections={sections}
|
||||
layouts={layouts}
|
||||
isSectioned={isSectioned}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PanelsAndSectionsLayout;
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
.body {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Trash2, X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
|
||||
import styles from './ConfirmDeleteDialog.module.scss';
|
||||
|
||||
interface ConfirmDeleteDialogProps {
|
||||
open: boolean;
|
||||
title: string;
|
||||
description: ReactNode;
|
||||
confirmLabel?: string;
|
||||
isLoading?: boolean;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared destructive-confirm dialog built on @signozhq/ui DialogWrapper (not
|
||||
* antd Modal), so it inherits the design-system styling/theme. Used by the
|
||||
* dashboard and section delete flows.
|
||||
*/
|
||||
function ConfirmDeleteDialog({
|
||||
open,
|
||||
title,
|
||||
description,
|
||||
confirmLabel = 'Delete',
|
||||
isLoading = false,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: ConfirmDeleteDialogProps): JSX.Element {
|
||||
const footer = (
|
||||
<div className={styles.footer}>
|
||||
<Button variant="solid" color="secondary" onClick={onClose}>
|
||||
<X size={12} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
loading={isLoading}
|
||||
onClick={onConfirm}
|
||||
testId="confirm-delete"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<DialogWrapper
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
title={title}
|
||||
width="narrow"
|
||||
showCloseButton={false}
|
||||
footer={footer}
|
||||
>
|
||||
<div className={styles.body}>{description}</div>
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfirmDeleteDialog;
|
||||
@@ -5,23 +5,26 @@
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
max-width: 80%;
|
||||
padding-left: 8px;
|
||||
|
||||
.linkToPreviousPage {
|
||||
// Collapse the design-system Button's fixed-height/padding box so it hugs
|
||||
// the label like inline text (the breadcrumb is text, not a chunky button).
|
||||
--button-height: auto;
|
||||
--button-padding: 0;
|
||||
--button-gap: 4px;
|
||||
.dashboardBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
padding: 0px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.currentPage {
|
||||
.dashboardBtn:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.idBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
@@ -43,9 +46,12 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.currentPage:hover {
|
||||
:global(.ant-btn-icon) {
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
}
|
||||
.idBtn:hover {
|
||||
background: color-mix(in srgb, var(--bg-robin-400) 10%, transparent);
|
||||
color: var(--bg-robin-300);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import { useCallback } from 'react';
|
||||
import { LayoutGrid } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import getSessionStorageApi from 'api/browser/sessionstorage/get';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY } from 'hooks/dashboard/useDashboardsListQueryParams';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { LayoutGrid } from '@signozhq/icons';
|
||||
|
||||
import styles from './DashboardBreadcrumbs.module.scss';
|
||||
|
||||
interface DashboardBreadcrumbsProps {
|
||||
interface Props {
|
||||
title: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
function DashboardBreadcrumbs({
|
||||
title,
|
||||
image,
|
||||
}: DashboardBreadcrumbsProps): JSX.Element {
|
||||
function DashboardBreadcrumbs({ title, image }: Props): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const goToListPage = useCallback(() => {
|
||||
@@ -39,23 +35,20 @@ function DashboardBreadcrumbs({
|
||||
<div className={styles.dashboardBreadcrumbs}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
prefix={<LayoutGrid size={14} />}
|
||||
className={styles.dashboardBtn}
|
||||
onClick={goToListPage}
|
||||
className={styles.linkToPreviousPage}
|
||||
testId="dashboard-breadcrumb-list"
|
||||
>
|
||||
Dashboard
|
||||
Dashboard /
|
||||
</Button>
|
||||
<div>/</div>
|
||||
<div className={styles.currentPage}>
|
||||
<Button variant="ghost" className={styles.idBtn}>
|
||||
<img
|
||||
src={image}
|
||||
alt="dashboard-icon"
|
||||
className={styles.dashboardIconImage}
|
||||
/>
|
||||
<Typography.Text>{title}</Typography.Text>
|
||||
</div>
|
||||
{title}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import DashboardBreadcrumbs from './DashboardBreadcrumbs';
|
||||
|
||||
import styles from './DashboardHeader.module.scss';
|
||||
|
||||
interface DashboardHeaderProps {
|
||||
interface Props {
|
||||
title: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
function DashboardHeader({ title, image }: DashboardHeaderProps): JSX.Element {
|
||||
function DashboardHeader({ title, image }: Props): JSX.Element {
|
||||
return (
|
||||
<div className={styles.dashboardHeader}>
|
||||
<DashboardBreadcrumbs title={title} image={image} />
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useEffect, useMemo } from 'react';
|
||||
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
|
||||
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import PanelTypeSelectionModal from 'container/DashboardContainer/PanelTypeSelectionModal';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
@@ -11,15 +10,12 @@ import PanelsAndSectionsLayout from './PanelsAndSectionsLayout';
|
||||
import { useDashboardStore } from './store/useDashboardStore';
|
||||
import styles from './DashboardContainer.module.scss';
|
||||
|
||||
interface DashboardContainerProps {
|
||||
interface Props {
|
||||
dashboard: DashboardtypesGettableDashboardV2DTO;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
function DashboardContainer({
|
||||
dashboard,
|
||||
refetch,
|
||||
}: DashboardContainerProps): JSX.Element {
|
||||
function DashboardContainer({ dashboard, refetch }: Props): JSX.Element {
|
||||
const fullScreenHandle = useFullScreenHandle();
|
||||
|
||||
const { user } = useAppContext();
|
||||
@@ -47,9 +43,6 @@ function DashboardContainer({
|
||||
/>
|
||||
<PanelsAndSectionsLayout layouts={layouts} panels={panels} />
|
||||
</div>
|
||||
{/* Shared panel-type picker (V1 component): opened from any "New Panel"
|
||||
trigger; navigates to the widget editor route on selection. */}
|
||||
<PanelTypeSelectionModal />
|
||||
</FullScreen>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,93 @@ import (
|
||||
)
|
||||
|
||||
func (provider *provider) addDashboardRoutes(router *mux.Router) error {
|
||||
// Saved-view routes are registered before `/api/v2/dashboards/{id}` so the
|
||||
// literal `views` segment isn't swallowed by the `{id}` pattern.
|
||||
if err := router.Handle("/api/v2/dashboards/views", handler.New(provider.authzMiddleware.ViewAccess(provider.dashboardHandler.ListViews), handler.OpenAPIDef{
|
||||
ID: "ListDashboardViews",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "List dashboard saved views",
|
||||
Description: "Returns every saved view in the calling user's org. Saved views are shared org-wide; any user may read, create, edit, and delete any view.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(dashboardtypes.ListableDashboardView),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/views", handler.New(provider.authzMiddleware.ViewAccess(provider.dashboardHandler.CreateView), handler.OpenAPIDef{
|
||||
ID: "CreateDashboardView",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Create dashboard saved view",
|
||||
Description: "Persists the calling user's dashboard listing state (query, sort, order) as a named, reusable view shared across the org.",
|
||||
Request: new(dashboardtypes.PostableDashboardView),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(dashboardtypes.GettableDashboardView),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/views/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.dashboardHandler.UpdateView), handler.OpenAPIDef{
|
||||
ID: "UpdateDashboardView",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Update dashboard saved view",
|
||||
Description: "Replaces a saved view's name and data. Saved views are shared org-wide; any user in the org may edit any view.",
|
||||
Request: new(dashboardtypes.UpdateableDashboardView),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(dashboardtypes.GettableDashboardView),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/views/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.dashboardHandler.DeleteView), handler.OpenAPIDef{
|
||||
ID: "DeleteDashboardView",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Delete dashboard saved view",
|
||||
Description: "Removes a saved view. Saved views are shared org-wide; any user in the org may delete any view. Idempotent — deleting a non-existent view returns 404.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards", handler.New(provider.authzMiddleware.ViewAccess(provider.dashboardHandler.ListV2), handler.OpenAPIDef{
|
||||
ID: "ListDashboardsV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "List dashboards (v2)",
|
||||
Description: "Returns a page of v2-shape dashboards for the calling user's org. Supports a filter DSL (`query`), sort (`updated_at`/`created_at`/`name`), order (`asc`/`desc`), and offset-based pagination (`limit`/`offset`).",
|
||||
Request: new(dashboardtypes.ListDashboardsV2Params),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(dashboardtypes.ListableDashboardV2),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards", handler.New(provider.authzMiddleware.EditAccess(provider.dashboardHandler.CreateV2), handler.OpenAPIDef{
|
||||
ID: "CreateDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
@@ -89,6 +176,23 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.dashboardHandler.DeleteV2), handler.OpenAPIDef{
|
||||
ID: "DeleteDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Delete dashboard (v2)",
|
||||
Description: "This endpoint deletes a v2-shape dashboard along with its tag relations. Locked dashboards are rejected.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}/lock", handler.New(provider.authzMiddleware.EditAccess(provider.dashboardHandler.LockV2), handler.OpenAPIDef{
|
||||
ID: "LockDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
@@ -123,6 +227,42 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ViewAccess: pinning only mutates the calling user's pin list, not the
|
||||
// dashboard itself — anyone who can view a dashboard can bookmark it.
|
||||
if err := router.Handle("/api/v2/dashboards/{id}/pins/me", handler.New(provider.authzMiddleware.ViewAccess(provider.dashboardHandler.PinV2), handler.OpenAPIDef{
|
||||
ID: "PinDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Pin a dashboard for the current user (v2)",
|
||||
Description: "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.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/dashboards/{id}/pins/me", handler.New(provider.authzMiddleware.ViewAccess(provider.dashboardHandler.UnpinV2), handler.OpenAPIDef{
|
||||
ID: "UnpinDashboardV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Unpin a dashboard for the current user (v2)",
|
||||
Description: "Removes the pin for the calling user. Idempotent — unpinning a dashboard that wasn't pinned still returns 204.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.CreatePublic), handler.OpenAPIDef{
|
||||
ID: "CreatePublicDashboard",
|
||||
Tags: []string{"dashboard"},
|
||||
|
||||
@@ -67,24 +67,5 @@ func (provider *provider) addTraceDetailRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v3/traces/{traceID}/flamegraph", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.traceDetailHandler.GetFlamegraph),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetFlamegraph",
|
||||
Tags: []string{"tracedetail"},
|
||||
Summary: "Get flamegraph view for a trace",
|
||||
Description: "Returns the flamegraph view of spans for a given trace ID.",
|
||||
Request: new(spantypes.PostableFlamegraph),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(spantypes.GettableFlamegraphTrace),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
},
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
@@ -15,7 +14,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/http/client"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -31,13 +29,12 @@ var scopes []string = []string{"email", "profile"}
|
||||
var _ authn.CallbackAuthN = (*AuthN)(nil)
|
||||
|
||||
type AuthN struct {
|
||||
store authtypes.AuthNStore
|
||||
settings factory.ScopedProviderSettings
|
||||
httpClient *client.Client
|
||||
globalConfig global.Config
|
||||
store authtypes.AuthNStore
|
||||
settings factory.ScopedProviderSettings
|
||||
httpClient *client.Client
|
||||
}
|
||||
|
||||
func New(ctx context.Context, store authtypes.AuthNStore, providerSettings factory.ProviderSettings, globalConfig global.Config) (*AuthN, error) {
|
||||
func New(ctx context.Context, store authtypes.AuthNStore, providerSettings factory.ProviderSettings) (*AuthN, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/authn/callbackauthn/googlecallbackauthn")
|
||||
|
||||
httpClient, err := client.New(settings.Logger(), providerSettings.TracerProvider, providerSettings.MeterProvider)
|
||||
@@ -46,10 +43,9 @@ func New(ctx context.Context, store authtypes.AuthNStore, providerSettings facto
|
||||
}
|
||||
|
||||
return &AuthN{
|
||||
store: store,
|
||||
settings: settings,
|
||||
httpClient: httpClient,
|
||||
globalConfig: globalConfig,
|
||||
store: store,
|
||||
settings: settings,
|
||||
httpClient: httpClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -182,7 +178,7 @@ func (a *AuthN) oauth2Config(siteURL *url.URL, authDomain *authtypes.AuthDomain,
|
||||
RedirectURL: (&url.URL{
|
||||
Scheme: siteURL.Scheme,
|
||||
Host: siteURL.Host,
|
||||
Path: path.Join(a.globalConfig.ExternalPath(), redirectPath),
|
||||
Path: redirectPath,
|
||||
}).String(),
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
<svg id="uuid-c6c3f75e-5369-448e-b895-3f99fb11bebe" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path d="M7.456.608c-.902-.411-1.909-.559-2.898-.417.053.041.086.107.082.179l-.082,1.405c.879-.183,1.827-.043,2.65.469.338.21.639.474.892.781,0,0,.024.027.061.069.091.104.26.299.334.402.006.031-.004.062-.026.084-.001.001-.002.002-.003.004l-.052.048-.765.681c-.039.035-.042.095-.007.134.017.019.04.03.065.031l1.107.065,1.402.082c.072.004.138-.029.179-.083.025-.033.041-.073.044-.117l.147-2.513c.003-.052-.037-.097-.089-.1-.025-.001-.049.007-.068.024l-.764.682v.003c-.106-.164-.22-.319-.34-.467-.516-.636-1.159-1.122-1.869-1.445Z" fill="#0078d4"/><path d="M4.441.147L1.932,0c-.052-.003-.097.037-.1.09-.001.025.007.049.024.068l.681.766h.003c-.159.104-.311.214-.455.331-.629.509-1.111,1.143-1.436,1.842-.424.913-.578,1.937-.434,2.942.041-.053.107-.086.179-.082l1.402.082c-.183-.881-.043-1.83.468-2.655.209-.338.473-.64.78-.893,0,0,.029-.026.072-.064.104-.092.297-.259.399-.332.031-.006.062.004.084.026.001.001.002.002.003.003l.048.052.679.766c.035.039.095.042.134.008.019-.017.03-.04.031-.065l.064-1.109.082-1.405c.004-.072-.029-.138-.082-.179-.033-.025-.073-.041-.117-.044Z" fill="#46a0de"/><path d="M10.411,5.611c.025-.363.013-.73-.039-1.095-.041.053-.107.086-.179.082l-1.402-.082c.038.186.062.374.071.564l1.55.53Z" fill="#155ea1"/><path d="M3.576,9.604l.271-.049,1.845-.343c-.095-.084-.155-.206-.155-.34v-.025c-.733.051-1.487-.119-2.159-.536-.338-.21-.639-.474-.892-.781,0,0-.024-.027-.061-.069-.091-.104-.26-.299-.334-.402-.006-.031.004-.062.026-.084.001-.001.002-.002.003-.004l.052-.048.765-.681c.039-.035.042-.095.007-.134-.017-.019-.04-.03-.065-.031l-1.107-.065-1.402-.082c-.072-.004-.138.029-.179.083-.025.033-.041.073-.044.117L0,8.645c-.003.052.037.097.089.1.025.001.049-.007.068-.024l.764-.682v-.003c.106.164.22.319.34.467.516.636,1.159,1.122,1.869,1.445.026.012.053.021.08.033.029-.188.173-.342.365-.376Z" fill="#8dc8e8"/><g><polygon points="8.241 5.343 5.968 5.765 5.968 8.87 8.241 9.355 10.522 8.44 10.522 6.123 8.241 5.343" fill="#8661c5"/><path d="M8.328,9.307l2.082-.844c.048-.019.084-.061.095-.111v-2.102c-.004-.064-.044-.119-.103-.143l-2.106-.716h-.095l-2.066.382c-.066.017-.114.075-.119.143v2.81c-.002.073.048.136.119.151l2.09.438c.035.004.07.002.103-.008Z" fill="none"/><path d="M5.968,5.765v3.105l2.297.486v-3.98l-2.297.39ZM6.938,8.631l-.644-.127v-2.388l.644-.103v2.619ZM7.939,8.814l-.739-.119v-2.73l.739-.127v2.977Z" fill="#56407f"/><polygon points="13.16 5.383 10.887 5.805 10.887 8.909 13.16 9.395 15.433 8.471 15.433 6.163 13.16 5.383" fill="#8661c5"/><path d="M10.887,5.805v3.105l2.281.486v-3.98l-2.281.39ZM11.849,8.67l-.644-.127v-2.388l.644-.103v2.619ZM12.85,8.854l-.739-.119v-2.73l.739-.135v2.985Z" fill="#56407f"/><polygon points="5.912 9.626 3.639 10.048 3.639 13.152 5.912 13.638 8.193 12.722 8.193 10.406 5.912 9.626" fill="#8661c5"/><path d="M3.632,10.048v3.081l2.297.486v-3.98l-2.297.414ZM4.593,12.921l-.644-.135v-2.388l.644-.111v2.635ZM5.602,13.128l-.739-.119v-2.762l.739-.127v3.009Z" fill="#56407f"/><polygon points="10.816 9.594 8.543 10.016 8.543 13.12 10.816 13.614 13.089 12.69 13.089 10.374 10.816 9.594" fill="#8661c5"/><path d="M8.543,10.016v3.112l2.289.486v-3.98l-2.289.382ZM9.504,12.889l-.644-.135v-2.388l.644-.111v2.635ZM10.506,13.065l-.739-.119v-2.73l.739-.127v2.977Z" fill="#56407f"/><polygon points="15.719 9.634 13.446 10.056 13.446 13.16 15.719 13.646 18 12.73 18 10.414 15.719 9.634" fill="#8661c5"/><path d="M13.446,10.056v3.073l2.297.486v-3.98l-2.297.422ZM14.416,12.929l-.644-.135v-2.388l.644-.111v2.635ZM15.417,13.104l-.739-.119v-2.73l.739-.127v2.977Z" fill="#56407f"/><polygon points="8.185 13.956 5.912 14.37 5.912 17.475 8.185 17.968 10.466 17.045 10.466 14.736 8.185 13.956" fill="#8661c5"/><path d="M8.273,17.904l2.074-.796c.06-.021.099-.08.095-.143v-2.07c.012-.076-.031-.149-.103-.175l-2.098-.716c-.031-.012-.065-.012-.095,0l-2.066.374c-.074.012-.128.076-.127.151v2.818c-.002.073.048.136.119.151l2.09.406c.036.012.075.012.111,0Z" fill="none"/><path d="M5.912,14.37v3.105l2.297.494v-4.044l-2.297.446ZM6.882,17.244l-.644-.135v-2.388l.644-.111v2.635ZM7.883,17.427l-.739-.119v-2.738l.739-.127v2.985Z" fill="#56407f"/><polygon points="13.097 13.988 10.824 14.41 10.824 17.514 13.097 18 15.377 17.085 15.377 14.768 13.097 13.988" fill="#8661c5"/><path d="M10.824,14.41v3.105l2.297.486v-3.98l-2.297.39ZM11.793,17.284l-.644-.135v-2.388l.644-.111v2.635ZM12.795,17.459l-.739-.119v-2.73l.739-.127v2.977Z" fill="#56407f"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 4.4 KiB |
@@ -1,293 +0,0 @@
|
||||
{
|
||||
"id": "aks",
|
||||
"title": "Azure Kubernetes Service (AKS)",
|
||||
"icon": "file://icon.svg",
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
{
|
||||
"name": "azure_kube_pod_status_ready_average",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_kube_pod_status_ready_total",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_kube_pod_status_phase_average",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_kube_pod_status_phase_total",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_kube_node_status_condition_average",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_kube_node_status_condition_total",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_cpu_usage_millicores_average",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_cpu_usage_millicores_maximum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_cpu_usage_percentage_average",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_cpu_usage_percentage_maximum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_disk_usage_bytes_average",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_disk_usage_bytes_maximum",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_disk_usage_percentage_average",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_disk_usage_percentage_maximum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_memory_rss_bytes_average",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_memory_rss_bytes_maximum",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_memory_rss_percentage_average",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_memory_rss_percentage_maximum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_memory_working_set_bytes_average",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_memory_working_set_bytes_maximum",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_memory_working_set_percentage_average",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_memory_working_set_percentage_maximum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_network_in_bytes_average",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_network_in_bytes_maximum",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_network_out_bytes_average",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_node_network_out_bytes_maximum",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_apiserver_current_inflight_requests_average",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_apiserver_current_inflight_requests_total",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_apiserver_cpu_usage_percentage_average",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_apiserver_cpu_usage_percentage_maximum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_apiserver_memory_usage_percentage_average",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_apiserver_memory_usage_percentage_maximum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_etcd_cpu_usage_percentage_average",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_etcd_cpu_usage_percentage_maximum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_etcd_database_usage_percentage_average",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_etcd_database_usage_percentage_maximum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_etcd_memory_usage_percentage_average",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_etcd_memory_usage_percentage_maximum",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_kube_node_status_allocatable_cpu_cores_average",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_kube_node_status_allocatable_cpu_cores_total",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_kube_node_status_allocatable_memory_bytes_average",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "azure_kube_node_status_allocatable_memory_bytes_total",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"logs": [
|
||||
{
|
||||
"name": "Resource ID",
|
||||
"path": "resources.azure.resource.id",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"azure": {
|
||||
"resourceProvider": "Microsoft.ContainerService",
|
||||
"resourceType": "managedClusters",
|
||||
"metrics": {},
|
||||
"logs": {
|
||||
"categoryGroups": ["allLogs"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
{
|
||||
"id": "overview",
|
||||
"title": "Azure Kubernetes Service (AKS) Overview",
|
||||
"description": "Overview of Azure Kubernetes Service (AKS) metrics",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
### Monitor Azure Kubernetes Service (AKS) with SigNoz
|
||||
|
||||
Collect key AKS metrics and view them with an out of the box dashboard.
|
||||
@@ -61,11 +61,27 @@ type Module interface {
|
||||
|
||||
GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error)
|
||||
|
||||
ListV2(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, params *dashboardtypes.ListDashboardsV2Params) (*dashboardtypes.ListableDashboardV2, error)
|
||||
|
||||
UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatable dashboardtypes.UpdatableDashboardV2) (*dashboardtypes.DashboardV2, error)
|
||||
|
||||
LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error
|
||||
|
||||
PatchV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, patch dashboardtypes.PatchableDashboardV2) (*dashboardtypes.DashboardV2, error)
|
||||
|
||||
PinV2(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, id valuer.UUID) error
|
||||
|
||||
UnpinV2(ctx context.Context, userID valuer.UUID, id valuer.UUID) error
|
||||
|
||||
DeleteV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
|
||||
|
||||
CreateView(ctx context.Context, orgID valuer.UUID, postable dashboardtypes.PostableDashboardView) (*dashboardtypes.DashboardView, error)
|
||||
|
||||
ListViews(ctx context.Context, orgID valuer.UUID) (*dashboardtypes.ListableDashboardView, error)
|
||||
|
||||
UpdateView(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updateable dashboardtypes.UpdateableDashboardView) (*dashboardtypes.DashboardView, error)
|
||||
|
||||
DeleteView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
@@ -96,6 +112,8 @@ type Handler interface {
|
||||
|
||||
GetV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
ListV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
UpdateV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
LockV2(http.ResponseWriter, *http.Request)
|
||||
@@ -103,4 +121,18 @@ type Handler interface {
|
||||
UnlockV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
PatchV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
PinV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
UnpinV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
DeleteV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
CreateView(http.ResponseWriter, *http.Request)
|
||||
|
||||
ListViews(http.ResponseWriter, *http.Request)
|
||||
|
||||
UpdateView(http.ResponseWriter, *http.Request)
|
||||
|
||||
DeleteView(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package impldashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes/listfilter"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
@@ -63,6 +65,96 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
|
||||
return storableDashboard, nil
|
||||
}
|
||||
|
||||
// ListV2 emits the joined dashboard ⨝ pinned_dashboard query the spec calls
|
||||
// for. Aliases:
|
||||
//
|
||||
// dashboard — the visitor expects this
|
||||
// pinned_dashboard AS pin — only used inside this query
|
||||
//
|
||||
// Sort is "is_pinned DESC, <sort> <order>" so pinned dashboards float to the
|
||||
// top inside the requested ordering. Name-sort goes through the same
|
||||
// JSONExtractString path the visitor uses for name/description filtering.
|
||||
func (store *store) ListV2(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
userID valuer.UUID,
|
||||
params *dashboardtypes.ListDashboardsV2Params,
|
||||
) ([]*dashboardtypes.DashboardListRow, int64, error) {
|
||||
compiled, err := listfilter.Compile(params.Query, store.sqlstore.Formatter())
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
type listedRow struct {
|
||||
*dashboardtypes.StorableDashboard `bun:",extend"`
|
||||
|
||||
IsPinned bool `bun:"is_pinned"`
|
||||
Total int64 `bun:"total"`
|
||||
}
|
||||
|
||||
rows := make([]*listedRow, 0)
|
||||
|
||||
q := store.sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(&rows).
|
||||
ColumnExpr("dashboard.id, dashboard.org_id, dashboard.name, dashboard.data, dashboard.locked, dashboard.source, dashboard.created_at, dashboard.created_by, dashboard.updated_at, dashboard.updated_by").
|
||||
ColumnExpr("CASE WHEN pin.user_id IS NOT NULL THEN 1 ELSE 0 END AS is_pinned").
|
||||
ColumnExpr("COUNT(*) OVER () AS total").
|
||||
Join("LEFT JOIN pinned_dashboard AS pin ON pin.user_id = ? AND pin.dashboard_id = dashboard.id", userID).
|
||||
Where("dashboard.org_id = ?", orgID).
|
||||
Where("dashboard.source != ?", dashboardtypes.SourceSystem)
|
||||
|
||||
if compiled != nil {
|
||||
q = q.Where(compiled.SQL, compiled.Args...)
|
||||
}
|
||||
|
||||
sortExpr, err := store.sortExprForListV2(params.Sort)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
q = q.
|
||||
OrderExpr("is_pinned DESC").
|
||||
OrderExpr(sortExpr + " " + strings.ToUpper(params.Order.StringValue())).
|
||||
Limit(params.Limit).
|
||||
Offset(params.Offset)
|
||||
|
||||
if err := q.Scan(ctx); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// COUNT(*) OVER () is computed pre-LIMIT, so any returned row carries the
|
||||
// full filter total. Empty result page => zero matches.
|
||||
var total int64
|
||||
if len(rows) > 0 {
|
||||
total = rows[0].Total
|
||||
}
|
||||
|
||||
out := make([]*dashboardtypes.DashboardListRow, len(rows))
|
||||
for i, r := range rows {
|
||||
out[i] = &dashboardtypes.DashboardListRow{
|
||||
Dashboard: r.StorableDashboard,
|
||||
Pinned: r.IsPinned,
|
||||
}
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
// sortExprForListV2 maps a sort enum to the SQL expression to plug into
|
||||
// ORDER BY. Title-sort routes through the SQLFormatter so it stays
|
||||
// dialect-aware (matches what listfilter/visitor does for the name filter).
|
||||
func (store *store) sortExprForListV2(sort dashboardtypes.ListSort) (string, error) {
|
||||
switch sort {
|
||||
case dashboardtypes.ListSortUpdatedAt:
|
||||
return "dashboard.updated_at", nil
|
||||
case dashboardtypes.ListSortCreatedAt:
|
||||
return "dashboard.created_at", nil
|
||||
case dashboardtypes.ListSortName:
|
||||
return string(store.sqlstore.Formatter().JSONExtractString("dashboard.data", "$.spec.display.name")), nil
|
||||
}
|
||||
return "", errors.Newf(errors.TypeInvalidInput, dashboardtypes.ErrCodeDashboardListInvalid,
|
||||
"unsupported sort field %q", sort)
|
||||
}
|
||||
|
||||
func (store *store) GetPublic(ctx context.Context, dashboardID string) (*dashboardtypes.StorablePublicDashboard, error) {
|
||||
storable := new(dashboardtypes.StorablePublicDashboard)
|
||||
err := store.
|
||||
@@ -217,3 +309,133 @@ func (store *store) RunInTx(ctx context.Context, cb func(ctx context.Context) er
|
||||
return cb(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
// PinForUser combines the count check, the existence check, and the upsert in
|
||||
// a single statement so the limit gate and the insert can't drift between two
|
||||
// round-trips.
|
||||
//
|
||||
// pin exists? | count < 10? | WHERE passes? | effect | rows
|
||||
// ------------|-------------|-------------------------|-----------------------------------|-----
|
||||
// no | yes | yes (count branch) | INSERT new row | 1
|
||||
// no | no | no | nothing (limit hit) | 0
|
||||
// yes | yes | yes (count branch) | INSERT → conflict → no-op UPDATE | 1
|
||||
// yes | no | yes (EXISTS OR branch) | INSERT → conflict → no-op UPDATE | 1
|
||||
//
|
||||
// rows = 0 is the only signal of a real limit hit.
|
||||
func (store *store) PinForUser(ctx context.Context, pd *dashboardtypes.PinnedDashboard) error {
|
||||
res, err := store.sqlstore.BunDBCtx(ctx).NewRaw(`
|
||||
INSERT INTO pinned_dashboard (user_id, dashboard_id, org_id, pinned_at)
|
||||
SELECT ?, ?, ?, ?
|
||||
WHERE (SELECT COUNT(*) FROM pinned_dashboard WHERE user_id = ?) < ?
|
||||
OR EXISTS (SELECT 1 FROM pinned_dashboard WHERE user_id = ? AND dashboard_id = ?)
|
||||
ON CONFLICT (user_id, dashboard_id) DO UPDATE SET user_id = EXCLUDED.user_id
|
||||
`,
|
||||
pd.UserID, pd.DashboardID, pd.OrgID, pd.PinnedAt,
|
||||
pd.UserID, dashboardtypes.MaxPinnedDashboardsPerUser,
|
||||
pd.UserID, pd.DashboardID,
|
||||
).Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rows == 0 {
|
||||
return errors.Newf(errors.TypeAlreadyExists, dashboardtypes.ErrCodePinnedDashboardLimitHit,
|
||||
"cannot pin more than %d dashboards", dashboardtypes.MaxPinnedDashboardsPerUser)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) UnpinForUser(ctx context.Context, userID valuer.UUID, dashboardID valuer.UUID) error {
|
||||
_, err := store.sqlstore.BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
Model((*dashboardtypes.PinnedDashboard)(nil)).
|
||||
Where("user_id = ?", userID).
|
||||
Where("dashboard_id = ?", dashboardID).
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (store *store) CreateDashboardView(ctx context.Context, view *dashboardtypes.DashboardView) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewInsert().
|
||||
Model(view).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "dashboard view with id %s already exists", view.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) GetDashboardView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardView, error) {
|
||||
view := new(dashboardtypes.DashboardView)
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(view).
|
||||
Where("id = ?", id).
|
||||
Where("org_id = ?", orgID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, dashboardtypes.ErrCodeDashboardViewNotFound, "dashboard view with id %s doesn't exist", id)
|
||||
}
|
||||
return view, nil
|
||||
}
|
||||
|
||||
func (store *store) ListDashboardViews(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.DashboardView, error) {
|
||||
views := make([]*dashboardtypes.DashboardView, 0)
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(&views).
|
||||
Where("org_id = ?", orgID).
|
||||
OrderExpr("updated_at DESC").
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return views, nil
|
||||
}
|
||||
|
||||
func (store *store) UpdateDashboardView(ctx context.Context, view *dashboardtypes.DashboardView) error {
|
||||
res, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewUpdate().
|
||||
Model(view).
|
||||
WherePK().
|
||||
Where("org_id = ?", view.OrgID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rows == 0 {
|
||||
return errors.Newf(errors.TypeNotFound, dashboardtypes.ErrCodeDashboardViewNotFound, "dashboard view with id %s doesn't exist", view.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) DeleteDashboardView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
Model(new(dashboardtypes.DashboardView)).
|
||||
Where("id = ?", id).
|
||||
Where("org_id = ?", orgID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return store.sqlstore.WrapNotFoundErrf(err, dashboardtypes.ErrCodeDashboardViewNotFound, "dashboard view with id %s doesn't exist", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -42,6 +42,38 @@ func (handler *handler) CreateV2(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Success(rw, http.StatusCreated, dashboard.ToGettableDashboardV2())
|
||||
}
|
||||
|
||||
func (handler *handler) ListV2(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
userID := valuer.MustNewUUID(claims.IdentityID())
|
||||
|
||||
params := new(dashboardtypes.ListDashboardsV2Params)
|
||||
if err := binding.Query.BindQuery(r.URL.Query(), params); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
if err := params.Validate(); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := handler.module.ListV2(ctx, orgID, userID, params)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (handler *handler) GetV2(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
@@ -205,3 +237,79 @@ func (handler *handler) PatchV2(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboard.ToGettableDashboardV2())
|
||||
}
|
||||
|
||||
func (handler *handler) PinV2(rw http.ResponseWriter, r *http.Request) {
|
||||
handler.pinUnpinV2(rw, r, true)
|
||||
}
|
||||
|
||||
func (handler *handler) UnpinV2(rw http.ResponseWriter, r *http.Request) {
|
||||
handler.pinUnpinV2(rw, r, false)
|
||||
}
|
||||
|
||||
func (handler *handler) pinUnpinV2(rw http.ResponseWriter, r *http.Request, pin bool) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
userID := valuer.MustNewUUID(claims.IdentityID())
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if pin {
|
||||
err = handler.module.PinV2(ctx, orgID, userID, dashboardID)
|
||||
} else {
|
||||
err = handler.module.UnpinV2(ctx, userID, dashboardID)
|
||||
}
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) DeleteV2(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := handler.module.DeleteV2(ctx, orgID, dashboardID); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package impldashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
@@ -42,6 +43,29 @@ func (m *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy stri
|
||||
return dashboard, nil
|
||||
}
|
||||
|
||||
func (module *module) ListV2(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, params *dashboardtypes.ListDashboardsV2Params) (*dashboardtypes.ListableDashboardV2, error) {
|
||||
rows, total, err := module.store.ListV2(ctx, orgID, userID, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dashboardIDs := make([]valuer.UUID, len(rows))
|
||||
for i, r := range rows {
|
||||
dashboardIDs[i] = r.Dashboard.ID
|
||||
}
|
||||
tagsByDashboard, err := module.tagModule.ListForResources(ctx, orgID, coretypes.KindDashboard, dashboardIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allTags, err := module.tagModule.List(ctx, orgID, coretypes.KindDashboard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dashboardtypes.NewListableDashboardV2(rows, total, tagsByDashboard, allTags)
|
||||
}
|
||||
|
||||
func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
|
||||
storable, err := module.store.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
@@ -135,6 +159,24 @@ func (module *module) PatchV2(ctx context.Context, orgID valuer.UUID, id valuer.
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
func (module *module) DeleteV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
existing, err := module.GetV2(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := existing.CanDelete(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
// Syncing to an empty tag set drops every tag link for the dashboard.
|
||||
if _, err := module.tagModule.SyncTags(ctx, orgID, coretypes.KindDashboard, id, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return module.store.Delete(ctx, orgID, id)
|
||||
})
|
||||
}
|
||||
|
||||
func (module *module) LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
|
||||
existing, err := module.GetV2(ctx, orgID, id)
|
||||
if err != nil {
|
||||
@@ -149,3 +191,19 @@ func (module *module) LockUnlockV2(ctx context.Context, orgID valuer.UUID, id va
|
||||
}
|
||||
return module.store.Update(ctx, orgID, storable)
|
||||
}
|
||||
|
||||
func (module *module) PinV2(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, id valuer.UUID) error {
|
||||
if _, err := module.GetV2(ctx, orgID, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return module.store.PinForUser(ctx, &dashboardtypes.PinnedDashboard{
|
||||
UserID: userID,
|
||||
DashboardID: id,
|
||||
OrgID: orgID,
|
||||
PinnedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func (module *module) UnpinV2(ctx context.Context, userID valuer.UUID, id valuer.UUID) error {
|
||||
return module.store.UnpinForUser(ctx, userID, id)
|
||||
}
|
||||
|
||||
132
pkg/modules/dashboard/impldashboard/v2_view_handler.go
Normal file
132
pkg/modules/dashboard/impldashboard/v2_view_handler.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package impldashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (handler *handler) CreateView(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
var req dashboardtypes.PostableDashboardView
|
||||
if err := binding.JSON.BindBody(r.Body, &req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
view, err := handler.module.CreateView(ctx, orgID, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusCreated, view)
|
||||
}
|
||||
|
||||
func (handler *handler) ListViews(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
out, err := handler.module.ListViews(ctx, orgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (handler *handler) UpdateView(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
viewID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
var req dashboardtypes.UpdateableDashboardView
|
||||
if err := binding.JSON.BindBody(r.Body, &req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
view, err := handler.module.UpdateView(ctx, orgID, viewID, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, view)
|
||||
}
|
||||
|
||||
func (handler *handler) DeleteView(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
viewID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := handler.module.DeleteView(ctx, orgID, viewID); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
46
pkg/modules/dashboard/impldashboard/v2_view_module.go
Normal file
46
pkg/modules/dashboard/impldashboard/v2_view_module.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package impldashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
func (module *module) CreateView(ctx context.Context, orgID valuer.UUID, postable dashboardtypes.PostableDashboardView) (*dashboardtypes.DashboardView, error) {
|
||||
if err := postable.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view := postable.NewDashboardView(orgID)
|
||||
if err := module.store.CreateDashboardView(ctx, view); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return view, nil
|
||||
}
|
||||
|
||||
func (module *module) ListViews(ctx context.Context, orgID valuer.UUID) (*dashboardtypes.ListableDashboardView, error) {
|
||||
views, err := module.store.ListDashboardViews(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dashboardtypes.ListableDashboardView{Views: views}, nil
|
||||
}
|
||||
|
||||
func (module *module) UpdateView(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updateable dashboardtypes.UpdateableDashboardView) (*dashboardtypes.DashboardView, error) {
|
||||
if err := updateable.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view, err := module.store.GetDashboardView(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view.Update(updateable)
|
||||
if err := module.store.UpdateDashboardView(ctx, view); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return view, nil
|
||||
}
|
||||
|
||||
func (module *module) DeleteView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
return module.store.DeleteDashboardView(ctx, orgID, id)
|
||||
}
|
||||
@@ -4,11 +4,9 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/session"
|
||||
@@ -17,12 +15,11 @@ import (
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
module session.Module
|
||||
globalConfig global.Config
|
||||
module session.Module
|
||||
}
|
||||
|
||||
func NewHandler(module session.Module, globalConfig global.Config) session.Handler {
|
||||
return &handler{module: module, globalConfig: globalConfig}
|
||||
func NewHandler(module session.Module) session.Handler {
|
||||
return &handler{module: module}
|
||||
}
|
||||
|
||||
func (handler *handler) GetSessionContext(rw http.ResponseWriter, req *http.Request) {
|
||||
@@ -161,13 +158,13 @@ func (handler *handler) DeleteSession(rw http.ResponseWriter, req *http.Request)
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) getRedirectURLFromErr(err error) string {
|
||||
func (*handler) getRedirectURLFromErr(err error) string {
|
||||
values := errors.AsURLValues(err)
|
||||
values.Add("callbackauthnerr", "true")
|
||||
|
||||
return (&url.URL{
|
||||
// When UI is being served on a prefix, we need to redirect to the login page on the prefix.
|
||||
Path: path.Join(handler.globalConfig.ExternalPath(), "/login"),
|
||||
Path: "/login",
|
||||
RawQuery: values.Encode(),
|
||||
}).String()
|
||||
}
|
||||
|
||||
@@ -67,6 +67,10 @@ func (m *module) syncLinksForResource(ctx context.Context, orgID valuer.UUID, ki
|
||||
})
|
||||
}
|
||||
|
||||
func (m *module) List(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind) ([]*tagtypes.Tag, error) {
|
||||
return m.store.List(ctx, orgID, kind)
|
||||
}
|
||||
|
||||
func (m *module) ListForResource(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID) ([]*tagtypes.Tag, error) {
|
||||
return m.store.ListByResource(ctx, orgID, kind, resourceID)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ type Module interface {
|
||||
// and reconciles the resource's links to exactly that set, all in one transaction.
|
||||
SyncTags(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID, postable []tagtypes.PostableTag) ([]*tagtypes.Tag, error)
|
||||
|
||||
// List returns every tag of the given kind in the org.
|
||||
List(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind) ([]*tagtypes.Tag, error)
|
||||
|
||||
ListForResource(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID) ([]*tagtypes.Tag, error)
|
||||
|
||||
// Resources with no tags are absent from the returned map.
|
||||
|
||||
@@ -6,16 +6,7 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Waterfall WaterfallConfig `mapstructure:"waterfall"`
|
||||
Flamegraph FlamegraphConfig `mapstructure:"flamegraph"`
|
||||
}
|
||||
|
||||
type FlamegraphConfig struct {
|
||||
MaxSelectedLevels int `mapstructure:"max_selected_levels"`
|
||||
MaxSpansPerLevel int `mapstructure:"max_spans_per_level"`
|
||||
SamplingTopLatencySpansCount int `mapstructure:"sampling_top_latency_count"`
|
||||
SamplingBucketCount int `mapstructure:"sampling_bucket_count"`
|
||||
SelectAllSpansLimit uint `mapstructure:"select_all_spans_limit"`
|
||||
Waterfall WaterfallConfig `mapstructure:"waterfall"`
|
||||
}
|
||||
|
||||
type WaterfallConfig struct {
|
||||
@@ -38,13 +29,6 @@ func newConfig() factory.Config {
|
||||
MaxDepthToAutoExpand: 5,
|
||||
MaxLimitToSelectAllSpans: 10_000,
|
||||
},
|
||||
Flamegraph: FlamegraphConfig{
|
||||
MaxSelectedLevels: 50,
|
||||
MaxSpansPerLevel: 100,
|
||||
SamplingTopLatencySpansCount: 5,
|
||||
SamplingBucketCount: 50,
|
||||
SelectAllSpansLimit: 100_000,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,20 +42,5 @@ func (c Config) Validate() error {
|
||||
if c.Waterfall.MaxLimitToSelectAllSpans == 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "traces.waterfall.max_limit_to_select_all_spans must be positive")
|
||||
}
|
||||
if c.Flamegraph.MaxSelectedLevels <= 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "tracedetail.flamegraph.level_limit must be positive, got %d", c.Flamegraph.MaxSelectedLevels)
|
||||
}
|
||||
if c.Flamegraph.MaxSpansPerLevel <= 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "tracedetail.flamegraph.spans_per_level must be positive, got %d", c.Flamegraph.MaxSpansPerLevel)
|
||||
}
|
||||
if c.Flamegraph.SamplingTopLatencySpansCount < 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "tracedetail.flamegraph.top_latency_count cannot be negative, got %d", c.Flamegraph.SamplingTopLatencySpansCount)
|
||||
}
|
||||
if c.Flamegraph.SamplingBucketCount <= 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "tracedetail.flamegraph.bucket_count must be positive, got %d", c.Flamegraph.SamplingBucketCount)
|
||||
}
|
||||
if c.Flamegraph.SelectAllSpansLimit == 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "tracedetail.flamegraph.max_limit_to_select_all_spans must be positive")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -80,19 +80,3 @@ func (h *handler) GetTraceAggregations(rw http.ResponseWriter, r *http.Request)
|
||||
|
||||
render.Success(rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *handler) GetFlamegraph(rw http.ResponseWriter, r *http.Request) {
|
||||
req := new(spantypes.PostableFlamegraph)
|
||||
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.module.GetFlamegraph(r.Context(), mux.Vars(r)["traceID"], req.SelectedSpanID, req.SelectFields)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/types/spantypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
|
||||
@@ -165,17 +164,6 @@ func (m *module) GetTraceAggregations(ctx context.Context, traceID string, req *
|
||||
return &spantypes.GettableTraceAggregations{Aggregations: results}, nil
|
||||
}
|
||||
|
||||
func (m *module) GetFlamegraph(ctx context.Context, traceID string, selectedSpanID string, selectFields []telemetrytypes.TelemetryFieldKey) (*spantypes.GettableFlamegraphTrace, error) {
|
||||
summary, err := m.store.GetTraceSummary(ctx, traceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if summary.NumSpans <= uint64(m.config.Flamegraph.SelectAllSpansLimit) {
|
||||
return m.getFullFlamegraph(ctx, traceID, summary, selectFields)
|
||||
}
|
||||
return m.getWindowedFlamegraph(ctx, traceID, selectedSpanID, summary, selectFields)
|
||||
}
|
||||
|
||||
// getWindowedWaterfall builds the waterfall tree with minimal data and then returns only a window of full spans.
|
||||
func (m *module) getWindowedWaterfall(ctx context.Context, traceID, selectedSpanID string, uncollapsedSpans []string, start, end time.Time) (*spantypes.GettableWaterfallTrace, error) {
|
||||
// Step 1: minimal fetch → build full tree → select visible window
|
||||
@@ -216,47 +204,3 @@ func (m *module) getWindowedWaterfall(ctx context.Context, traceID, selectedSpan
|
||||
waterfallTrace, selectedSpans, uncollapsedSpans, false, nil,
|
||||
), nil
|
||||
}
|
||||
|
||||
func (m *module) getFullFlamegraph(ctx context.Context, traceID string, summary *spantypes.TraceSummary, selectFields []telemetrytypes.TelemetryFieldKey) (*spantypes.GettableFlamegraphTrace, error) {
|
||||
fullSpans, err := m.store.GetFlamegraphSpans(ctx, traceID, summary.Start, summary.End, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(fullSpans) == 0 {
|
||||
return nil, spantypes.ErrTraceNotFound
|
||||
}
|
||||
flamegraphTrace := spantypes.NewFlamegraphTraceFromStorable(fullSpans, selectFields)
|
||||
return spantypes.NewGettableFlamegraphTrace(flamegraphTrace.GetAllLevels(), summary.Start.UnixMilli(), summary.End.UnixMilli(), false), nil
|
||||
}
|
||||
|
||||
// getWindowedFlamegraph returns a window of a max levels and max sampled spans per level around the selected span.
|
||||
func (m *module) getWindowedFlamegraph(ctx context.Context, traceID, selectedSpanID string, summary *spantypes.TraceSummary, selectFields []telemetrytypes.TelemetryFieldKey) (*spantypes.GettableFlamegraphTrace, error) {
|
||||
minimalSpans, err := m.store.GetMinimalSpans(ctx, traceID, summary.Start, summary.End)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(minimalSpans) == 0 {
|
||||
return nil, spantypes.ErrTraceNotFound
|
||||
}
|
||||
|
||||
flamegraphTrace := spantypes.NewFlamegraphTraceFromMinimal(minimalSpans)
|
||||
minimalSpans = nil //nolint:ineffassign,wastedassign // release backing array before further db calls
|
||||
|
||||
cfg := m.config.Flamegraph
|
||||
selectedSpans := flamegraphTrace.GetSelectedLevels(selectedSpanID, cfg.MaxSelectedLevels, cfg.MaxSpansPerLevel, cfg.SamplingTopLatencySpansCount, cfg.SamplingBucketCount)
|
||||
if len(selectedSpans) == 0 {
|
||||
return nil, spantypes.ErrTraceNotFound
|
||||
}
|
||||
|
||||
fullSpans, err := m.store.GetFlamegraphSpans(ctx, traceID, summary.Start, summary.End, spantypes.FlamegraphWindowSpanIDs(selectedSpans))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return spantypes.NewGettableFlamegraphTrace(
|
||||
flamegraphTrace.EnrichSelectedSpans(selectedSpans, fullSpans, selectFields),
|
||||
summary.Start.UnixMilli(),
|
||||
summary.End.UnixMilli(),
|
||||
true,
|
||||
), nil
|
||||
}
|
||||
|
||||
@@ -154,47 +154,6 @@ func (s *traceStore) GetTraceSpansByIDs(ctx context.Context, traceID string, sta
|
||||
return spans, nil
|
||||
}
|
||||
|
||||
func (s *traceStore) GetFlamegraphSpans(ctx context.Context, traceID string, start, end time.Time, spanIDs []string) ([]spantypes.StorableSpan, error) {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select(
|
||||
"span_id",
|
||||
"any(parent_span_id) AS parent_span_id",
|
||||
"any(timestamp) AS timestamp",
|
||||
"any(duration_nano) AS duration_nano",
|
||||
"any(has_error) AS has_error",
|
||||
"any(name) AS name",
|
||||
"any(events) AS events",
|
||||
"any(attributes_string) AS attributes_string",
|
||||
"any(attributes_number) AS attributes_number",
|
||||
"any(attributes_bool) AS attributes_bool",
|
||||
"any(resources_string) AS resources_string",
|
||||
)
|
||||
sb.From(fmt.Sprintf("%s.%s", spantypes.TraceDB, spantypes.TraceTable))
|
||||
conditions := []string{
|
||||
sb.E("trace_id", traceID),
|
||||
sb.GE("ts_bucket_start", start.Unix()-1800),
|
||||
sb.LE("ts_bucket_start", end.Unix()),
|
||||
}
|
||||
if len(spanIDs) > 0 {
|
||||
ids := make([]any, len(spanIDs))
|
||||
for i, id := range spanIDs {
|
||||
ids[i] = id
|
||||
}
|
||||
conditions = append(conditions, sb.In("span_id", ids...))
|
||||
}
|
||||
sb.Where(conditions...)
|
||||
sb.GroupBy("span_id")
|
||||
sb.OrderByAsc("timestamp")
|
||||
sb.OrderByAsc("name")
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
var spans []spantypes.StorableSpan
|
||||
if err := s.telemetryStore.ClickhouseDB().Select(ctx, &spans, query, args...); err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error querying flamegraph spans")
|
||||
}
|
||||
return spans, nil
|
||||
}
|
||||
|
||||
func (s *traceStore) GetSpanCountByField(ctx context.Context, traceID string, summary *spantypes.TraceSummary, fieldKey telemetrytypes.TelemetryFieldKey) (map[string]uint64, error) {
|
||||
fieldExpr, err := buildFieldExpr(fieldKey)
|
||||
if err != nil {
|
||||
|
||||
@@ -91,30 +91,6 @@ func TestGetSpanCountByField(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFlamegraphSpans(t *testing.T) {
|
||||
baseSQL := "SELECT span_id, any(parent_span_id) AS parent_span_id, any(timestamp) AS timestamp, any(duration_nano) AS duration_nano, any(has_error) AS has_error, any(name) AS name, any(events) AS events, any(attributes_string) AS attributes_string, any(attributes_number) AS attributes_number, any(attributes_bool) AS attributes_bool, any(resources_string) AS resources_string FROM signoz_traces.distributed_signoz_index_v3 WHERE trace_id = ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY span_id ORDER BY timestamp ASC, name ASC"
|
||||
withSpanIDsSQL := "SELECT span_id, any(parent_span_id) AS parent_span_id, any(timestamp) AS timestamp, any(duration_nano) AS duration_nano, any(has_error) AS has_error, any(name) AS name, any(events) AS events, any(attributes_string) AS attributes_string, any(attributes_number) AS attributes_number, any(attributes_bool) AS attributes_bool, any(resources_string) AS resources_string FROM signoz_traces.distributed_signoz_index_v3 WHERE trace_id = ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND span_id IN (?, ?) GROUP BY span_id ORDER BY timestamp ASC, name ASC"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
spanIDs []string
|
||||
sql string
|
||||
}{
|
||||
{name: "NoSpanIDs_GeneratesBaseSQL", spanIDs: nil, sql: baseSQL},
|
||||
{name: "WithSpanIDs_GeneratesInClauseSQL", spanIDs: []string{"span-1", "span-2"}, sql: withSpanIDsSQL},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := newTestStore(sqlmock.QueryMatcherRegexp)
|
||||
s.Mock().ExpectSelect(regexp.QuoteMeta(tc.sql)).
|
||||
WillReturnRows(cmock.NewRows(nil, nil))
|
||||
_, _ = s.Store().GetFlamegraphSpans(context.Background(), testTraceID, testStart, testEnd, tc.spanIDs)
|
||||
assert.NoError(t, s.Mock().ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSpanDurationByField(t *testing.T) {
|
||||
|
||||
expectedSQL := "WITH all_spans AS (SELECT DISTINCT ON (span_id) resource.`service.name`::String AS field_value, toUnixTimestamp64Nano(timestamp) AS start_ns, start_ns + duration_nano AS end_ns FROM signoz_traces.distributed_signoz_index_v3 WHERE trace_id = ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND notEmpty(field_value) ORDER BY timestamp ASC, name ASC), effective_start AS (SELECT field_value, end_ns, greatest(start_ns, ifNull(max(end_ns) OVER (PARTITION BY field_value ORDER BY start_ns ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), toUInt64(0))) AS effective_start_ns FROM all_spans) SELECT field_value, sum(toUInt64(greatest(end_ns - effective_start_ns, 0))) AS total_ns FROM effective_start GROUP BY field_value"
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/spantypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
// Handler exposes HTTP handlers for trace detail APIs.
|
||||
@@ -13,7 +12,6 @@ type Handler interface {
|
||||
GetWaterfall(http.ResponseWriter, *http.Request)
|
||||
GetWaterfallV4(http.ResponseWriter, *http.Request)
|
||||
GetTraceAggregations(http.ResponseWriter, *http.Request)
|
||||
GetFlamegraph(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
// Module defines the business logic for trace detail operations.
|
||||
@@ -21,5 +19,4 @@ type Module interface {
|
||||
GetWaterfall(ctx context.Context, traceID string, req *spantypes.PostableWaterfall) (*spantypes.GettableWaterfallTrace, error)
|
||||
GetWaterfallV4(ctx context.Context, traceID string, selectedSpanID string, uncollapsedSpans []string, selectAllLimit uint) (*spantypes.GettableWaterfallTrace, error)
|
||||
GetTraceAggregations(ctx context.Context, traceID string, req *spantypes.PostableTraceAggregations) (*spantypes.GettableTraceAggregations, error)
|
||||
GetFlamegraph(ctx context.Context, traceID string, selectedSpanID string, selectFields []telemetrytypes.TelemetryFieldKey) (*spantypes.GettableFlamegraphTrace, error)
|
||||
}
|
||||
|
||||
33
pkg/parser/filterquery/parse.go
Normal file
33
pkg/parser/filterquery/parse.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package filterquery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/filterquery/grammar"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
)
|
||||
|
||||
func Parse(query string) (antlr.ParseTree, *antlr.CommonTokenStream, *ErrorCollector) {
|
||||
collector := NewErrorCollector()
|
||||
lexer := grammar.NewFilterQueryLexer(antlr.NewInputStream(query))
|
||||
lexer.RemoveErrorListeners()
|
||||
lexer.AddErrorListener(collector)
|
||||
tokens := antlr.NewCommonTokenStream(lexer, 0)
|
||||
parser := grammar.NewFilterQueryParser(tokens)
|
||||
parser.RemoveErrorListeners()
|
||||
parser.AddErrorListener(collector)
|
||||
return parser.Query(), tokens, collector
|
||||
}
|
||||
|
||||
type ErrorCollector struct {
|
||||
*antlr.DefaultErrorListener
|
||||
Errors []string
|
||||
}
|
||||
|
||||
func NewErrorCollector() *ErrorCollector {
|
||||
return &ErrorCollector{}
|
||||
}
|
||||
|
||||
func (c *ErrorCollector) SyntaxError(_ antlr.Recognizer, _ any, line, column int, msg string, _ antlr.RecognitionException) {
|
||||
c.Errors = append(c.Errors, fmt.Sprintf("syntax error at %d:%d — %s", line, column, msg))
|
||||
}
|
||||
@@ -7,15 +7,14 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/authn/callbackauthn/googlecallbackauthn"
|
||||
"github.com/SigNoz/signoz/pkg/authn/passwordauthn/emailpasswordauthn"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
)
|
||||
|
||||
func NewAuthNs(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing, globalConfig global.Config) (map[authtypes.AuthNProvider]authn.AuthN, error) {
|
||||
func NewAuthNs(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
|
||||
emailPasswordAuthN := emailpasswordauthn.New(store)
|
||||
|
||||
googleCallbackAuthN, err := googlecallbackauthn.New(ctx, store, providerSettings, globalConfig)
|
||||
googleCallbackAuthN, err := googlecallbackauthn.New(ctx, store, providerSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -211,6 +211,8 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddDashboardNameFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewFixChangelogOperationTypeFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewCloudIntegrationRemoveCascadeDeleteFactory(sqlschema),
|
||||
sqlmigration.NewAddPinnedDashboardFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddDashboardViewFactory(sqlstore, sqlschema),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -275,14 +277,14 @@ func NewQuerierProviderFactories(telemetryStore telemetrystore.TelemetryStore, p
|
||||
)
|
||||
}
|
||||
|
||||
func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.AuthZ, modules Modules, handlers Handlers, globalConfig global.Config) factory.NamedMap[factory.ProviderFactory[apiserver.APIServer, apiserver.Config]] {
|
||||
func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.AuthZ, modules Modules, handlers Handlers) factory.NamedMap[factory.ProviderFactory[apiserver.APIServer, apiserver.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
signozapiserver.NewFactory(
|
||||
orgGetter,
|
||||
authz,
|
||||
implorganization.NewHandler(modules.OrgGetter, modules.OrgSetter),
|
||||
impluser.NewHandler(modules.UserSetter, modules.UserGetter),
|
||||
implsession.NewHandler(modules.Session, globalConfig),
|
||||
implsession.NewHandler(modules.Session),
|
||||
implauthdomain.NewHandler(modules.AuthDomain),
|
||||
implpreference.NewHandler(modules.Preference),
|
||||
handlers.Global,
|
||||
|
||||
@@ -95,7 +95,6 @@ func TestNewProviderFactories(t *testing.T) {
|
||||
nil,
|
||||
Modules{},
|
||||
Handlers{},
|
||||
global.Config{},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -542,7 +542,7 @@ func New(
|
||||
ctx,
|
||||
providerSettings,
|
||||
config.APIServer,
|
||||
NewAPIServerProviderFactories(orgGetter, authz, modules, handlers, config.Global),
|
||||
NewAPIServerProviderFactories(orgGetter, authz, modules, handlers),
|
||||
"signoz",
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
67
pkg/sqlmigration/092_add_pinned_dashboard.go
Normal file
67
pkg/sqlmigration/092_add_pinned_dashboard.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addPinnedDashboard struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
func NewAddPinnedDashboardFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_pinned_dashboard"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addPinnedDashboard{
|
||||
sqlstore: sqlstore,
|
||||
sqlschema: sqlschema,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addPinnedDashboard) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addPinnedDashboard) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
sqls := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
|
||||
Name: "pinned_dashboard",
|
||||
Columns: []*sqlschema.Column{
|
||||
{Name: "user_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "dashboard_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "pinned_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false, Default: "current_timestamp"},
|
||||
},
|
||||
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"user_id", "dashboard_id"}},
|
||||
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
|
||||
{
|
||||
ReferencingColumnName: sqlschema.ColumnName("org_id"),
|
||||
ReferencedTableName: sqlschema.TableName("organizations"),
|
||||
ReferencedColumnName: sqlschema.ColumnName("id"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addPinnedDashboard) Down(_ context.Context, _ *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
69
pkg/sqlmigration/093_add_dashboard_view.go
Normal file
69
pkg/sqlmigration/093_add_dashboard_view.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addDashboardView struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
func NewAddDashboardViewFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_dashboard_view"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addDashboardView{
|
||||
sqlstore: sqlstore,
|
||||
sqlschema: sqlschema,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addDashboardView) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addDashboardView) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
sqls := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
|
||||
Name: "dashboard_view",
|
||||
Columns: []*sqlschema.Column{
|
||||
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "name", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "data", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
|
||||
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
|
||||
},
|
||||
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"id"}},
|
||||
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
|
||||
{
|
||||
ReferencingColumnName: sqlschema.ColumnName("org_id"),
|
||||
ReferencedTableName: sqlschema.TableName("organizations"),
|
||||
ReferencedColumnName: sqlschema.ColumnName("id"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addDashboardView) Down(_ context.Context, _ *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -27,7 +27,6 @@ var (
|
||||
// Azure services.
|
||||
AzureServiceStorageAccountsBlob = ServiceID{valuer.NewString("storageaccountsblob")}
|
||||
AzureServiceCDNProfile = ServiceID{valuer.NewString("cdnprofile")}
|
||||
AzureServiceAKS = ServiceID{valuer.NewString("aks")}
|
||||
)
|
||||
|
||||
func (ServiceID) Enum() []any {
|
||||
@@ -47,7 +46,6 @@ func (ServiceID) Enum() []any {
|
||||
AWSServiceSQS,
|
||||
AzureServiceStorageAccountsBlob,
|
||||
AzureServiceCDNProfile,
|
||||
AzureServiceAKS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +69,6 @@ var SupportedServices = map[CloudProviderType][]ServiceID{
|
||||
CloudProviderTypeAzure: {
|
||||
AzureServiceStorageAccountsBlob,
|
||||
AzureServiceCDNProfile,
|
||||
AzureServiceAKS,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
146
pkg/types/dashboardtypes/dashboard_view.go
Normal file
146
pkg/types/dashboardtypes/dashboard_view.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
const (
|
||||
DashboardViewSchemaVersion = "v1"
|
||||
MaxDashboardViewNameLen = 32
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeDashboardViewInvalidInput = errors.MustNewCode("dashboard_view_invalid_input")
|
||||
ErrCodeDashboardViewNotFound = errors.MustNewCode("dashboard_view_not_found")
|
||||
)
|
||||
|
||||
type DashboardViewData struct {
|
||||
Version string `json:"version" required:"true"`
|
||||
Query string `json:"query"`
|
||||
Sort ListSort `json:"sort"`
|
||||
Order ListOrder `json:"order"`
|
||||
}
|
||||
|
||||
func (d *DashboardViewData) Validate() error {
|
||||
if d.Version != DashboardViewSchemaVersion {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardViewInvalidInput,
|
||||
"version must be %q, got %q", DashboardViewSchemaVersion, d.Version)
|
||||
}
|
||||
if !d.Sort.IsZero() {
|
||||
s := d.Sort
|
||||
switch s {
|
||||
case ListSortUpdatedAt, ListSortCreatedAt, ListSortName:
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardViewInvalidInput,
|
||||
"invalid sort %q — expected one of: `updated_at`, `created_at`, `name`", d.Sort)
|
||||
}
|
||||
d.Sort = s
|
||||
}
|
||||
if !d.Order.IsZero() {
|
||||
o := d.Order
|
||||
switch o {
|
||||
case ListOrderAsc, ListOrderDesc:
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardViewInvalidInput,
|
||||
"invalid order %q — expected `asc` or `desc`", d.Order)
|
||||
}
|
||||
d.Order = o
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DashboardView struct {
|
||||
bun.BaseModel `bun:"table:dashboard_view,alias:dashboard_view"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
|
||||
Name string `bun:"name,type:text,notnull" json:"name" required:"true"`
|
||||
Data DashboardViewData `bun:"data,type:jsonb,notnull" json:"data" required:"true"`
|
||||
OrgID valuer.UUID `bun:"org_id,type:text,notnull" json:"orgId" required:"true"`
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// Postable
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
type PostableDashboardView struct {
|
||||
Name string `json:"name" required:"true"`
|
||||
Data DashboardViewData `json:"data" required:"true"`
|
||||
}
|
||||
|
||||
func (p *PostableDashboardView) UnmarshalJSON(data []byte) error {
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.DisallowUnknownFields()
|
||||
type alias PostableDashboardView
|
||||
var tmp alias
|
||||
if err := dec.Decode(&tmp); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardViewInvalidInput, "invalid saved view request body").WithAdditional(err.Error())
|
||||
}
|
||||
*p = PostableDashboardView(tmp)
|
||||
return p.Validate()
|
||||
}
|
||||
|
||||
func (p *PostableDashboardView) Validate() error {
|
||||
if err := validateDashboardViewName(p.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.Data.Validate()
|
||||
}
|
||||
|
||||
func (p PostableDashboardView) NewDashboardView(orgID valuer.UUID) *DashboardView {
|
||||
now := time.Now()
|
||||
return &DashboardView{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
TimeAuditable: types.TimeAuditable{CreatedAt: now, UpdatedAt: now},
|
||||
OrgID: orgID,
|
||||
Name: p.Name,
|
||||
Data: p.Data,
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// Updateable
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
type UpdateableDashboardView = PostableDashboardView
|
||||
|
||||
func (v *DashboardView) Update(updateable UpdateableDashboardView) {
|
||||
v.Name = updateable.Name
|
||||
v.Data = updateable.Data
|
||||
v.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// Gettable
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
type GettableDashboardView = DashboardView
|
||||
|
||||
type ListableDashboardView struct {
|
||||
Views []*GettableDashboardView `json:"views" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// Helpers
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
func validateDashboardViewName(name string) error {
|
||||
trimmed := strings.TrimSpace(name)
|
||||
if trimmed == "" {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardViewInvalidInput, "name is required")
|
||||
}
|
||||
if len(trimmed) > MaxDashboardViewNameLen {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardViewInvalidInput,
|
||||
"name must be at most %d characters, got %d", MaxDashboardViewNameLen, len(trimmed))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user