Compare commits

..

77 Commits

Author SHA1 Message Date
Piyush Singariya
9ee23eae06 Merge branch 'main' into merge-json-col-fields 2026-03-13 23:43:27 +05:30
Ishan
2316b5be83 Sig 3634 revert (#10578)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* Revert "Revert "feat: Option to zoom out OR reset zoom in the explorer pages (#10464)" (#10574)"

This reverts commit 5b8d5fbfd3.

* fix: stop bubble
2026-03-13 15:29:28 +00:00
Piyush Singariya
0f36479787 Merge branch 'main' into merge-json-col-fields 2026-03-13 19:55:05 +05:30
Piyush Singariya
95a9a24875 fix: message field key search in JSON Logs (#10577)
* feat: work in progress

* fix: test run success

* fix: in progress

* fix: excluding message from metadata fetch

* test: cleared

* fix: key name in metadata

* fix: uncomment tests

* chore: change to method for staticfields

* fix: remove confusing comments; remove usage of logical keyword

* chore: shift method above business logic

* chore: changes based on review

* fix: comments in metadata_store.go
2026-03-13 19:16:05 +05:30
Abhi kumar
937ebc1582 feat: added section in panel settings (#10569)
* feat: added section in panel settings

* chore: minor changes

* fix: fixed failing tests

* fix: minor style fixes

* chore: updated the categorisation

* chore: updated styles

* chore: minor styles improvements

* chore: formatting unit section fix
2026-03-13 13:22:10 +00:00
Ashwin Bhatkal
dcc8173c79 fix: variables initial url state (#10579)
* fix: variables-initial-url-state

* chore: add tests
2026-03-13 11:16:47 +00:00
Ashwin Bhatkal
4b4ef5ce58 fix: edit mode variables not persisting value (#10576)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix: edit mode variables not persisting value

* chore: move into hook

* chore: add tests

* chore: fix tests

* chore: move functions
2026-03-13 07:49:40 +00:00
Piyush Singariya
7d1e39037c chore: minor changes based on review 2026-03-13 13:06:56 +05:30
Piyush Singariya
54f104db5f fix: cursor comments 2026-03-13 12:15:12 +05:30
Yunus M
5b8d5fbfd3 Revert "feat: Option to zoom out OR reset zoom in the explorer pages (#10464)" (#10574)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
This reverts commit 557451ed81.
2026-03-12 19:24:49 +00:00
Ashwin Bhatkal
0271be11e6 chore: remove dashboard provider from the root (#10526)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: remove dashboard provider from the root

* chore: fix tests

* chore: fix tests

* chore: remove dashboardId from provider

* chore: remove old instances of dashboard provider

* chore: separate dashboard widget fully

* chore: fix tests

* chore: resolve self comments
2026-03-12 14:51:49 +00:00
Vikrant Gupta
92d220c4d9 feat(serviceaccount): domain changes for service account (#10568)
* feat(serviceaccount): domain type changes

* feat(serviceaccount): domain type changes

* feat(serviceaccount): domain type changes
2026-03-12 11:06:04 +00:00
Vikrant Gupta
0ed8169bad feat(authz): add service account authz changes (#10567) 2026-03-12 09:42:50 +00:00
SagarRajput-7
ed553fb02e feat: removed plan name and added copiable license info in custom domain card (#10558)
* feat: removed plan name and added copiable license info in custom domain card

* feat: added condition on the license row in custom domain card

* feat: code refactor and making license row a common component

* feat: added test case and addressed feedback

* feat: style improvement

* feat: added maskedkey util and refactored code

* feat: updated test case
2026-03-12 09:24:41 +00:00
Ashwin Bhatkal
47daba3c17 chore: link session url with sentry alert (#10566) 2026-03-12 09:19:31 +00:00
Srikanth Chekuri
2b3310809a fix: newServer uses the stored config hash for mismatch (#10563) 2026-03-12 08:26:22 +00:00
Ashwin Bhatkal
542a648cc3 chore: remove toScrollWidgetId from dashboard provider (#10562)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: remove toScrollWidgetId from dashboard provider

* chore: remove dead files

* chore: fix tests
2026-03-12 06:03:02 +00:00
Piyush Singariya
ab0852bbfb feat: update message as typehint in JSON Column (#10545) 2026-03-11 14:50:59 +05:30
Piyush Singariya
4c7aba680e fix: lint error 2026-03-10 14:00:12 +05:30
Piyush Singariya
23c247a1ba Merge branch 'main' into merge-json-col-fields 2026-03-10 13:28:30 +05:30
Piyush Singariya
4777b13ddf fix: shift warning attachment to getKeySelectors 2026-03-03 13:27:23 +05:30
Piyush Singariya
2d3060bac4 chore: remove unnecessary change 2026-02-25 12:09:30 +05:30
Piyush Singariya
9101d51920 Merge branch 'main' into merge-json-col-fields 2026-02-23 12:50:11 +05:30
Piyush Singariya
82b82b0208 chore: addressing comments from Nitya 2026-02-23 12:49:48 +05:30
Nityananda Gohain
51bd760d9a Merge branch 'main' into merge-json-col-fields 2026-02-20 15:53:31 +05:30
Piyush Singariya
2a492cc783 fix: change warning to a const to fix tests 2026-02-20 14:54:16 +05:30
Piyush Singariya
24afdad36c fix: append warnings from fieldkeys 2026-02-20 14:51:13 +05:30
Piyush Singariya
5d20019207 fix: body.message not being mapped correctly 2026-02-20 14:26:12 +05:30
Piyush Singariya
1963d5811d Merge branch 'main' into merge-json-col-fields 2026-02-20 10:08:25 +05:30
Piyush Singariya
15cfccad74 revert: change ReadMultiple is needed 2026-02-20 10:08:11 +05:30
Piyush Singariya
a0399560e3 revert: remvoing unused function 2026-02-19 16:33:12 +05:30
Piyush Singariya
265e337d5c Merge branch 'main' into merge-json-col-fields 2026-02-19 16:29:55 +05:30
Piyush Singariya
bb8c874755 fix: go lint 2026-02-17 17:19:24 +05:30
Piyush Singariya
13cbe03d64 fix: tests 2026-02-17 16:58:00 +05:30
Piyush Singariya
93621c29b7 fix: go mod changes 2026-02-17 16:29:00 +05:30
Piyush Singariya
2c691b5a75 fix: test fixed 2026-02-17 16:28:54 +05:30
Piyush Singariya
cd7e1bb114 Merge branch 'main' into merge-json-col-fields 2026-02-17 16:22:58 +05:30
Piyush Singariya
a1d2ec8b8a fix: remove unused function 2026-02-17 16:18:38 +05:30
Piyush Singariya
8bbafb52d5 fix: go.mod required changes 2026-02-17 16:16:17 +05:30
Piyush Singariya
075cfab463 feat: mapping body_v2.message:string map to body 2026-02-17 13:26:34 +05:30
Piyush Singariya
86bccaac0c test: blocked on pr #10153 2026-02-16 15:24:31 +05:30
Piyush Singariya
de1aac63c0 revert: more unrelated change 2026-02-16 13:19:49 +05:30
Piyush Singariya
14fe8745b5 Merge branch 'main' into merge-json-col-fields 2026-02-16 13:14:39 +05:30
Piyush Singariya
4013c7ee03 revert: few unrelated changes 2026-02-16 13:13:07 +05:30
Piyush Singariya
0d34360e0b fix: handle datatype collision 2026-01-30 12:17:28 +05:30
srikanthccv
d204c89dec Merge branch 'main' into merge-json-col-fields 2026-01-30 02:12:14 +05:30
Piyush Singariya
8dd33c1ab7 Merge branch 'main' into merge-json-col-fields 2026-01-29 19:57:22 +05:30
Piyush Singariya
8e5c3d5ae1 chore: merge json fields 2026-01-29 16:46:00 +05:30
Piyush Singariya
d45bb52f33 Merge branch 'has-jsonqb' into merge-json-col-fields 2026-01-29 13:21:13 +05:30
Piyush Singariya
e71818292d fix: go test flakiness 2026-01-29 10:17:53 +05:30
Piyush Singariya
37557f7f24 Merge branch 'main' into has-jsonqb 2026-01-29 09:12:22 +05:30
Piyush Singariya
27ff102660 Merge branch 'main' into has-jsonqb 2026-01-28 17:44:48 +05:30
Piyush Singariya
cb2aa4cffd fix: tests 2026-01-28 17:42:17 +05:30
Piyush Singariya
58d1d84ec7 test: fix 2026-01-28 15:34:22 +05:30
Piyush Singariya
d8e116a7bc fix: merge conflict 2026-01-28 15:15:50 +05:30
Piyush Singariya
6a48bdc37e Merge branch 'main' into has-jsonqb 2026-01-28 15:15:17 +05:30
Piyush Singariya
ffb62432f8 chore: var renamed 2026-01-28 14:42:51 +05:30
Piyush Singariya
57c51f070c fix: merge json body columns together 2026-01-28 14:36:15 +05:30
Piyush Singariya
36becfc7a2 fix: removed comment 2026-01-27 13:20:52 +05:30
Piyush Singariya
8e71de09f3 fix: remove unnecessary bool checking 2026-01-27 13:16:30 +05:30
Piyush Singariya
56de92de73 fix: changes based on review from Srikanth 2026-01-27 13:12:32 +05:30
Piyush Singariya
62b10f8e77 Merge branch 'main' into has-jsonqb 2026-01-27 10:04:32 +05:30
Piyush Singariya
20b53d7856 fix: review based on tushar 2026-01-27 10:04:15 +05:30
Piyush Singariya
8f2c506304 fix: json qb test fix 2026-01-22 22:00:06 +05:30
Srikanth Chekuri
7b5b9027dd Merge branch 'main' into has-jsonqb 2026-01-22 20:19:39 +05:30
Piyush Singariya
b77f97fcb7 fix: tests 2026-01-22 17:24:26 +05:30
Piyush Singariya
62942a4162 fix: tests 2026-01-22 15:47:20 +05:30
Piyush Singariya
349bbbbf1d Merge branch 'main' into has-jsonqb 2026-01-22 12:36:45 +05:30
Piyush Singariya
1966a7a5f6 fix: empty filteredArrays condition 2026-01-22 12:36:29 +05:30
Piyush Singariya
a4eed9ff13 fix: build json plans in metadata 2026-01-22 12:33:51 +05:30
Piyush Singariya
24d1ee33b5 revert: gitignore change 2026-01-22 10:39:02 +05:30
Srikanth Chekuri
3402203021 Merge branch 'main' into has-jsonqb 2026-01-21 13:47:39 +05:30
Piyush Singariya
e8e4897cc8 fix: tests GroupBy 2026-01-20 12:10:44 +05:30
Piyush Singariya
96fb88aaee fix: ignored .vscode in gitignore 2026-01-20 11:47:34 +05:30
Piyush Singariya
5a00e6c2cd Merge branch 'main' into has-jsonqb 2026-01-20 11:44:32 +05:30
Piyush Singariya
e2500cff7d fix: tests expected queries and values 2026-01-20 11:40:56 +05:30
Piyush Singariya
4864c3bc37 feat: has JSON QB 2026-01-20 11:23:50 +05:30
184 changed files with 5069 additions and 4993 deletions

5
.github/CODEOWNERS vendored
View File

@@ -127,12 +127,15 @@
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
# Dashboard Widget Page
/frontend/src/pages/DashboardWidget/ @SigNoz/pulse-frontend
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
## Dashboard Page
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
## Public Dashboard Page

View File

@@ -18,7 +18,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/queryparser"
@@ -26,7 +25,6 @@ import (
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/usertypes"
"github.com/SigNoz/signoz/pkg/version"
"github.com/SigNoz/signoz/pkg/zeus"
"github.com/SigNoz/signoz/pkg/zeus/noopzeus"
@@ -75,14 +73,14 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
},
signoz.NewSQLStoreProviderFactories(),
signoz.NewTelemetryStoreProviderFactories(),
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing, userStore usertypes.UserStore) (map[authtypes.AuthNProvider]authn.AuthN, error) {
return signoz.NewAuthNs(ctx, providerSettings, store, licensing, userStore)
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)
},
func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ dashboard.Module) factory.ProviderFactory[authz.AuthZ, authz.Config] {
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx))
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing, userGetter user.Getter) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, userGetter)
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)
},
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return noopgateway.NewProviderFactory()

View File

@@ -9,12 +9,12 @@ import (
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
eequerier "github.com/SigNoz/signoz/ee/querier"
"github.com/SigNoz/signoz/ee/authz/openfgaschema"
"github.com/SigNoz/signoz/ee/gateway/httpgateway"
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
eequerier "github.com/SigNoz/signoz/ee/querier"
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
"github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema"
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
@@ -29,7 +29,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/signoz"
@@ -37,7 +36,6 @@ import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/usertypes"
"github.com/SigNoz/signoz/pkg/version"
"github.com/SigNoz/signoz/pkg/zeus"
"github.com/spf13/cobra"
@@ -97,7 +95,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
},
sqlstoreFactories,
signoz.NewTelemetryStoreProviderFactories(),
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing, userStore usertypes.UserStore) (map[authtypes.AuthNProvider]authn.AuthN, error) {
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)
if err != nil {
return nil, err
@@ -108,7 +106,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return nil, err
}
authNs, err := signoz.NewAuthNs(ctx, providerSettings, store, licensing, userStore)
authNs, err := signoz.NewAuthNs(ctx, providerSettings, store, licensing)
if err != nil {
return nil, err
}
@@ -121,8 +119,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(ctx context.Context, sqlstore sqlstore.SQLStore, licensing licensing.Licensing, dashboardModule dashboard.Module) factory.ProviderFactory[authz.AuthZ, authz.Config] {
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), licensing, dashboardModule)
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, userGetter user.Getter) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing, userGetter)
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)
},
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return httpgateway.NewProviderFactory(licensing)

View File

@@ -1768,19 +1768,19 @@ components:
createdAt:
format: date-time
type: string
expires_at:
expiresAt:
minimum: 0
type: integer
id:
type: string
key:
type: string
last_observed_at:
lastObservedAt:
format: date-time
type: string
name:
type: string
service_account_id:
serviceAccountId:
type: string
updatedAt:
format: date-time
@@ -1788,9 +1788,9 @@ components:
required:
- id
- key
- expires_at
- last_observed_at
- service_account_id
- expiresAt
- lastObservedAt
- serviceAccountId
type: object
ServiceaccounttypesGettableFactorAPIKeyWithKey:
properties:
@@ -1804,14 +1804,14 @@ components:
type: object
ServiceaccounttypesPostableFactorAPIKey:
properties:
expires_at:
expiresAt:
minimum: 0
type: integer
name:
type: string
required:
- name
- expires_at
- expiresAt
type: object
ServiceaccounttypesPostableServiceAccount:
properties:
@@ -1842,7 +1842,7 @@ components:
type: string
name:
type: string
orgID:
orgId:
type: string
roles:
items:
@@ -1859,19 +1859,19 @@ components:
- email
- roles
- status
- orgID
- orgId
- deletedAt
type: object
ServiceaccounttypesUpdatableFactorAPIKey:
properties:
expires_at:
expiresAt:
minimum: 0
type: integer
name:
type: string
required:
- name
- expires_at
- expiresAt
type: object
ServiceaccounttypesUpdatableServiceAccount:
properties:
@@ -1993,6 +1993,43 @@ components:
userId:
type: string
type: object
TypesGettableAPIKey:
properties:
createdAt:
format: date-time
type: string
createdBy:
type: string
createdByUser:
$ref: '#/components/schemas/TypesUser'
expiresAt:
format: int64
type: integer
id:
type: string
lastUsed:
format: int64
type: integer
name:
type: string
revoked:
type: boolean
role:
type: string
token:
type: string
updatedAt:
format: date-time
type: string
updatedBy:
type: string
updatedByUser:
$ref: '#/components/schemas/TypesUser'
userId:
type: string
required:
- id
type: object
TypesGettableGlobalConfig:
properties:
external_url:
@@ -2054,6 +2091,16 @@ components:
required:
- id
type: object
TypesPostableAPIKey:
properties:
expiresInDays:
format: int64
type: integer
name:
type: string
role:
type: string
type: object
TypesPostableAcceptInvite:
properties:
displayName:
@@ -2118,6 +2165,33 @@ components:
required:
- id
type: object
TypesStorableAPIKey:
properties:
createdAt:
format: date-time
type: string
createdBy:
type: string
id:
type: string
name:
type: string
revoked:
type: boolean
role:
type: string
token:
type: string
updatedAt:
format: date-time
type: string
updatedBy:
type: string
userId:
type: string
required:
- id
type: object
TypesUser:
properties:
createdAt:
@@ -3791,6 +3865,222 @@ paths:
summary: Update org preference
tags:
- preferences
/api/v1/pats:
get:
deprecated: false
description: This endpoint lists all api keys
operationId: ListAPIKeys
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/TypesGettableAPIKey'
type: array
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:
- ADMIN
- tokenizer:
- ADMIN
summary: List api keys
tags:
- users
post:
deprecated: false
description: This endpoint creates an api key
operationId: CreateAPIKey
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesPostableAPIKey'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TypesGettableAPIKey'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Create api key
tags:
- users
/api/v1/pats/{id}:
delete:
deprecated: false
description: This endpoint revokes an api key
operationId: RevokeAPIKey
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Revoke api key
tags:
- users
put:
deprecated: false
description: This endpoint updates an api key
operationId: UpdateAPIKey
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesStorableAPIKey'
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:
- ADMIN
- tokenizer:
- ADMIN
summary: Update api key
tags:
- users
/api/v1/public/dashboards/{id}:
get:
deprecated: false

View File

@@ -13,20 +13,17 @@ import (
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer"
)
var (
TypeableResourcesRoles = authtypes.MustNewTypeableMetaResources(authtypes.MustNewName("roles"))
)
type provider struct {
pkgAuthzService authz.AuthZ
openfgaServer *openfgaserver.Server
licensing licensing.Licensing
store authtypes.RoleStore
store roletypes.Store
registry []authz.RegisterTypeable
}
@@ -85,23 +82,23 @@ func (provider *provider) Write(ctx context.Context, additions []*openfgav1.Tupl
return provider.openfgaServer.Write(ctx, additions, deletions)
}
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.Role, error) {
return provider.pkgAuthzService.Get(ctx, orgID, id)
}
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.Role, error) {
return provider.pkgAuthzService.GetByOrgIDAndName(ctx, orgID, name)
}
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.Role, error) {
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.Role, error) {
return provider.pkgAuthzService.List(ctx, orgID)
}
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.Role, error) {
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.Role, error) {
return provider.pkgAuthzService.ListByOrgIDAndNames(ctx, orgID, names)
}
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.Role, error) {
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*roletypes.Role, error) {
return provider.pkgAuthzService.ListByOrgIDAndIDs(ctx, orgID, ids)
}
@@ -117,7 +114,7 @@ func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, names [
return provider.pkgAuthzService.Revoke(ctx, orgID, names, subject)
}
func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.UUID, managedRoles []*authtypes.Role) error {
func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.UUID, managedRoles []*roletypes.Role) error {
return provider.pkgAuthzService.CreateManagedRoles(ctx, orgID, managedRoles)
}
@@ -139,16 +136,16 @@ func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context,
return provider.Write(ctx, tuples, nil)
}
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
return provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
return provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
}
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) (*authtypes.Role, error) {
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) (*roletypes.Role, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -162,10 +159,10 @@ func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, ro
}
if existingRole != nil {
return authtypes.NewRoleFromStorableRole(existingRole), nil
return roletypes.NewRoleFromStorableRole(existingRole), nil
}
err = provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
err = provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
if err != nil {
return nil, err
}
@@ -220,13 +217,13 @@ func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id
return objects, nil
}
func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
return provider.store.Update(ctx, orgID, authtypes.NewStorableRoleFromRole(role))
return provider.store.Update(ctx, orgID, roletypes.NewStorableRoleFromRole(role))
}
func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
@@ -235,12 +232,12 @@ func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, n
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
additionTuples, err := authtypes.GetAdditionTuples(name, orgID, relation, additions)
additionTuples, err := roletypes.GetAdditionTuples(name, orgID, relation, additions)
if err != nil {
return err
}
deletionTuples, err := authtypes.GetDeletionTuples(name, orgID, relation, deletions)
deletionTuples, err := roletypes.GetDeletionTuples(name, orgID, relation, deletions)
if err != nil {
return err
}
@@ -264,7 +261,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return err
}
role := authtypes.NewRoleFromStorableRole(storableRole)
role := roletypes.NewRoleFromStorableRole(storableRole)
err = role.ErrIfManaged()
if err != nil {
return err
@@ -274,7 +271,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
}
func (provider *provider) MustGetTypeables() []authtypes.Typeable {
return []authtypes.Typeable{authtypes.TypeableRole, TypeableResourcesRoles}
return []authtypes.Typeable{authtypes.TypeableRole, roletypes.TypeableResourcesRoles}
}
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) ([]*openfgav1.TupleKey, error) {
@@ -286,7 +283,7 @@ func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID va
adminSubject,
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName),
},
orgID,
)
@@ -301,7 +298,7 @@ func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID va
anonymousSubject,
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAnonymousRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAnonymousRoleName),
},
orgID,
)

View File

@@ -31,22 +31,9 @@ func (server *Server) Stop(ctx context.Context) error {
}
func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, _ []authtypes.Selector) error {
subject := ""
switch claims.Principal {
case authtypes.PrincipalUser.StringValue():
user, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
if err != nil {
return err
}
subject = user
case authtypes.PrincipalServiceAccount.StringValue():
serviceAccount, err := authtypes.NewSubject(authtypes.TypeableServiceAccount, claims.ServiceAccountID, orgID, nil)
if err != nil {
return err
}
subject = serviceAccount
subject, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
if err != nil {
return err
}
tupleSlice, err := typeable.Tuples(subject, relation, selectors, orgID)

View File

@@ -11,7 +11,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/types"
@@ -20,6 +19,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -31,9 +31,9 @@ type module struct {
licensing licensing.Licensing
}
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, userGetter user.Getter) dashboard.Module {
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard")
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser, userGetter)
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser)
return &module{
pkgDashboardModule: pkgDashboardModule,
@@ -214,8 +214,8 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.U
return module.pkgDashboardModule.Update(ctx, orgID, id, updatedBy, data, diff)
}
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, lock bool) error {
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, lock)
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, role types.Role, lock bool) error {
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, role, lock)
}
func (module *module) MustGetTypeables() []authtypes.Typeable {
@@ -224,7 +224,7 @@ func (module *module) MustGetTypeables() []authtypes.Typeable {
func (module *module) MustGetManagedRoleTransactions() map[string][]*authtypes.Transaction {
return map[string][]*authtypes.Transaction{
authtypes.SigNozAnonymousRoleName: {
roletypes.SigNozAnonymousRoleName: {
{
ID: valuer.GenerateUUID(),
Relation: authtypes.RelationRead,

View File

@@ -63,8 +63,6 @@ func (h *handler) QueryRange(rw http.ResponseWriter, req *http.Request) {
h.set.Logger.ErrorContext(ctx, "panic in QueryRange",
"error", r,
"user", claims.UserID,
"principal", claims.Principal,
"service_account", claims.ServiceAccountID,
"payload", string(queryJSON),
"stacktrace", stackTrace,
)

View File

@@ -12,9 +12,10 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/user"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
"go.uber.org/zap"
@@ -48,7 +49,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
return
}
apiKey, apiErr := ah.getOrCreateCloudIntegrationAPIKey(r.Context(), claims.OrgID, cloudProvider)
apiKey, apiErr := ah.getOrCreateCloudIntegrationPAT(r.Context(), claims.OrgID, cloudProvider)
if apiErr != nil {
RespondError(w, basemodel.WrapApiError(
apiErr, "couldn't provision PAT for cloud integration:",
@@ -108,25 +109,32 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
ah.Respond(w, result)
}
func (ah *APIHandler) getOrCreateCloudIntegrationAPIKey(ctx context.Context, orgId string, cloudProvider string) (
func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId string, cloudProvider string) (
string, *basemodel.ApiError,
) {
integrationPATName := fmt.Sprintf("%s integration", cloudProvider)
integrationServiceAccount, apiErr := ah.getOrCreateCloudIntegrationServiceAccount(ctx, orgId, cloudProvider)
integrationUser, apiErr := ah.getOrCreateCloudIntegrationUser(ctx, orgId, cloudProvider)
if apiErr != nil {
return "", apiErr
}
keys, err := ah.Signoz.Modules.ServiceAccount.ListFactorAPIKey(ctx, integrationServiceAccount.ID)
orgIdUUID, err := valuer.NewUUID(orgId)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't list api keys: %w", err,
"couldn't parse orgId: %w", err,
))
}
for _, key := range keys {
if key.Name == integrationPATName {
return key.Key, nil
allPats, err := ah.Signoz.Modules.User.ListAPIKeys(ctx, orgIdUUID)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't list PATs: %w", err,
))
}
for _, p := range allPats {
if p.UserID == integrationUser.ID && p.Name == integrationPATName {
return p.Token, nil
}
}
@@ -135,35 +143,46 @@ func (ah *APIHandler) getOrCreateCloudIntegrationAPIKey(ctx context.Context, org
zap.String("cloudProvider", cloudProvider),
)
apiKey, err := integrationServiceAccount.NewFactorAPIKey(integrationPATName, 0)
newPAT, err := types.NewStorableAPIKey(
integrationPATName,
integrationUser.ID,
types.RoleViewer,
0,
)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't create cloud integration PAT: %w", err,
))
}
err = ah.Signoz.Modules.ServiceAccount.CreateFactorAPIKey(ctx, apiKey)
err = ah.Signoz.Modules.User.CreateAPIKey(ctx, newPAT)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't create cloud integration api key: %w", err,
"couldn't create cloud integration PAT: %w", err,
))
}
return apiKey.Key, nil
return newPAT.Token, nil
}
func (ah *APIHandler) getOrCreateCloudIntegrationServiceAccount(
func (ah *APIHandler) getOrCreateCloudIntegrationUser(
ctx context.Context, orgId string, cloudProvider string,
) (*serviceaccounttypes.ServiceAccount, *basemodel.ApiError) {
serviceAccountName := fmt.Sprintf("%s-integration", cloudProvider)
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", serviceAccountName))
) (*types.User, *basemodel.ApiError) {
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider)
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName))
serviceAccount := serviceaccounttypes.NewServiceAccount(serviceAccountName, email, []string{authtypes.SigNozViewerRoleName}, serviceaccounttypes.StatusActive, valuer.MustNewUUID(orgId))
serviceAccount, err := ah.Signoz.Modules.ServiceAccount.GetOrCreate(ctx, serviceAccount)
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId), types.UserStatusActive)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't look for integration service account: %w", err))
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
}
return serviceAccount, nil
password := types.MustGenerateFactorPassword(cloudIntegrationUser.ID.StringValue())
cloudIntegrationUser, err = ah.Signoz.Modules.User.GetOrCreateUser(ctx, cloudIntegrationUser, user.WithFactorPassword(password))
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't look for integration user: %w", err))
}
return cloudIntegrationUser, nil
}
func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (

View File

@@ -216,7 +216,8 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
}),
otelmux.WithPublicEndpoint(),
))
r.Use(middleware.NewAuthN([]string{"Authorization", "Sec-WebSocket-Protocol"}, []string{"SIGNOZ-API-KEY"}, s.signoz.Sharder, s.signoz.Tokenizer, s.signoz.ServiceAccountTokenizer, s.signoz.Instrumentation.Logger()).Wrap)
r.Use(middleware.NewAuthN([]string{"Authorization", "Sec-WebSocket-Protocol"}, s.signoz.Sharder, s.signoz.Tokenizer, s.signoz.Instrumentation.Logger()).Wrap)
r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap)
r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(),
s.config.APIServer.Timeout.ExcludedRoutes,
s.config.APIServer.Timeout.Default,

View File

@@ -128,6 +128,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
isAdmin &&
(path === ROUTES.SETTINGS ||
path === ROUTES.ORG_SETTINGS ||
path === ROUTES.MEMBERS_SETTINGS ||
path === ROUTES.BILLING ||
path === ROUTES.MY_SETTINGS);

View File

@@ -29,7 +29,6 @@ import posthog from 'posthog-js';
import { useAppContext } from 'providers/App/App';
import { IUser } from 'providers/App/types';
import { CmdKProvider } from 'providers/cmdKProvider';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
@@ -321,6 +320,19 @@ function App(): JSX.Element {
// Session Replay
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
beforeSend(event) {
const sessionReplayUrl = posthog.get_session_replay_url?.({
withTimestamp: true,
});
if (sessionReplayUrl) {
// eslint-disable-next-line no-param-reassign
event.contexts = {
...event.contexts,
posthog: { session_replay_url: sessionReplayUrl },
};
}
return event;
},
});
setIsSentryInitialized(true);
@@ -371,28 +383,26 @@ function App(): JSX.Element {
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AppLayout>
<PreferenceContextProvider>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</PreferenceContextProvider>
</AppLayout>
</KeyboardHotkeysProvider>
</DashboardProvider>
<KeyboardHotkeysProvider>
<AppLayout>
<PreferenceContextProvider>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</PreferenceContextProvider>
</AppLayout>
</KeyboardHotkeysProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>

View File

@@ -2100,7 +2100,7 @@ export interface ServiceaccounttypesFactorAPIKeyDTO {
* @type integer
* @minimum 0
*/
expires_at: number;
expiresAt: number;
/**
* @type string
*/
@@ -2113,7 +2113,7 @@ export interface ServiceaccounttypesFactorAPIKeyDTO {
* @type string
* @format date-time
*/
last_observed_at: Date;
lastObservedAt: Date;
/**
* @type string
*/
@@ -2121,7 +2121,7 @@ export interface ServiceaccounttypesFactorAPIKeyDTO {
/**
* @type string
*/
service_account_id: string;
serviceAccountId: string;
/**
* @type string
* @format date-time
@@ -2145,7 +2145,7 @@ export interface ServiceaccounttypesPostableFactorAPIKeyDTO {
* @type integer
* @minimum 0
*/
expires_at: number;
expiresAt: number;
/**
* @type string
*/
@@ -2193,7 +2193,7 @@ export interface ServiceaccounttypesServiceAccountDTO {
/**
* @type string
*/
orgID: string;
orgId: string;
/**
* @type array
*/
@@ -2214,7 +2214,7 @@ export interface ServiceaccounttypesUpdatableFactorAPIKeyDTO {
* @type integer
* @minimum 0
*/
expires_at: number;
expiresAt: number;
/**
* @type string
*/
@@ -2345,6 +2345,63 @@ export interface TypesChangePasswordRequestDTO {
userId?: string;
}
export interface TypesGettableAPIKeyDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
createdBy?: string;
createdByUser?: TypesUserDTO;
/**
* @type integer
* @format int64
*/
expiresAt?: number;
/**
* @type string
*/
id: string;
/**
* @type integer
* @format int64
*/
lastUsed?: number;
/**
* @type string
*/
name?: string;
/**
* @type boolean
*/
revoked?: boolean;
/**
* @type string
*/
role?: string;
/**
* @type string
*/
token?: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
/**
* @type string
*/
updatedBy?: string;
updatedByUser?: TypesUserDTO;
/**
* @type string
*/
userId?: string;
}
export interface TypesGettableGlobalConfigDTO {
/**
* @type string
@@ -2438,6 +2495,22 @@ export interface TypesOrganizationDTO {
updatedAt?: Date;
}
export interface TypesPostableAPIKeyDTO {
/**
* @type integer
* @format int64
*/
expiresInDays?: number;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
role?: string;
}
export interface TypesPostableAcceptInviteDTO {
/**
* @type string
@@ -2529,6 +2602,51 @@ export interface TypesResetPasswordTokenDTO {
token?: string;
}
export interface TypesStorableAPIKeyDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name?: string;
/**
* @type boolean
*/
revoked?: boolean;
/**
* @type string
*/
role?: string;
/**
* @type string
*/
token?: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
/**
* @type string
*/
updatedBy?: string;
/**
* @type string
*/
userId?: string;
}
export interface TypesUserDTO {
/**
* @type string
@@ -2993,6 +3111,31 @@ export type GetOrgPreference200 = {
export type UpdateOrgPreferencePathParameters = {
name: string;
};
export type ListAPIKeys200 = {
/**
* @type array
*/
data: TypesGettableAPIKeyDTO[];
/**
* @type string
*/
status: string;
};
export type CreateAPIKey201 = {
data: TypesGettableAPIKeyDTO;
/**
* @type string
*/
status: string;
};
export type RevokeAPIKeyPathParameters = {
id: string;
};
export type UpdateAPIKeyPathParameters = {
id: string;
};
export type GetPublicDashboardDataPathParameters = {
id: string;
};

View File

@@ -22,6 +22,7 @@ import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
AcceptInvite201,
ChangePasswordPathParameters,
CreateAPIKey201,
CreateInvite201,
DeleteInvitePathParameters,
DeleteUserPathParameters,
@@ -32,16 +33,21 @@ import type {
GetResetPasswordTokenPathParameters,
GetUser200,
GetUserPathParameters,
ListAPIKeys200,
ListInvite200,
ListUsers200,
RenderErrorResponseDTO,
RevokeAPIKeyPathParameters,
TypesChangePasswordRequestDTO,
TypesPostableAcceptInviteDTO,
TypesPostableAPIKeyDTO,
TypesPostableBulkInviteRequestDTO,
TypesPostableForgotPasswordDTO,
TypesPostableInviteDTO,
TypesPostableResetPasswordDTO,
TypesStorableAPIKeyDTO,
TypesUserDTO,
UpdateAPIKeyPathParameters,
UpdateUser200,
UpdateUserPathParameters,
} from '../sigNoz.schemas';
@@ -744,6 +750,349 @@ export const useCreateBulkInvite = <
return useMutation(mutationOptions);
};
/**
* This endpoint lists all api keys
* @summary List api keys
*/
export const listAPIKeys = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListAPIKeys200>({
url: `/api/v1/pats`,
method: 'GET',
signal,
});
};
export const getListAPIKeysQueryKey = () => {
return [`/api/v1/pats`] as const;
};
export const getListAPIKeysQueryOptions = <
TData = Awaited<ReturnType<typeof listAPIKeys>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listAPIKeys>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListAPIKeysQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listAPIKeys>>> = ({
signal,
}) => listAPIKeys(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listAPIKeys>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListAPIKeysQueryResult = NonNullable<
Awaited<ReturnType<typeof listAPIKeys>>
>;
export type ListAPIKeysQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List api keys
*/
export function useListAPIKeys<
TData = Awaited<ReturnType<typeof listAPIKeys>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listAPIKeys>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListAPIKeysQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List api keys
*/
export const invalidateListAPIKeys = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListAPIKeysQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint creates an api key
* @summary Create api key
*/
export const createAPIKey = (
typesPostableAPIKeyDTO: BodyType<TypesPostableAPIKeyDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateAPIKey201>({
url: `/api/v1/pats`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableAPIKeyDTO,
signal,
});
};
export const getCreateAPIKeyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createAPIKey>>,
TError,
{ data: BodyType<TypesPostableAPIKeyDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createAPIKey>>,
TError,
{ data: BodyType<TypesPostableAPIKeyDTO> },
TContext
> => {
const mutationKey = ['createAPIKey'];
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 createAPIKey>>,
{ data: BodyType<TypesPostableAPIKeyDTO> }
> = (props) => {
const { data } = props ?? {};
return createAPIKey(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateAPIKeyMutationResult = NonNullable<
Awaited<ReturnType<typeof createAPIKey>>
>;
export type CreateAPIKeyMutationBody = BodyType<TypesPostableAPIKeyDTO>;
export type CreateAPIKeyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create api key
*/
export const useCreateAPIKey = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createAPIKey>>,
TError,
{ data: BodyType<TypesPostableAPIKeyDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createAPIKey>>,
TError,
{ data: BodyType<TypesPostableAPIKeyDTO> },
TContext
> => {
const mutationOptions = getCreateAPIKeyMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint revokes an api key
* @summary Revoke api key
*/
export const revokeAPIKey = ({ id }: RevokeAPIKeyPathParameters) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/pats/${id}`,
method: 'DELETE',
});
};
export const getRevokeAPIKeyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof revokeAPIKey>>,
TError,
{ pathParams: RevokeAPIKeyPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof revokeAPIKey>>,
TError,
{ pathParams: RevokeAPIKeyPathParameters },
TContext
> => {
const mutationKey = ['revokeAPIKey'];
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 revokeAPIKey>>,
{ pathParams: RevokeAPIKeyPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return revokeAPIKey(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type RevokeAPIKeyMutationResult = NonNullable<
Awaited<ReturnType<typeof revokeAPIKey>>
>;
export type RevokeAPIKeyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Revoke api key
*/
export const useRevokeAPIKey = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof revokeAPIKey>>,
TError,
{ pathParams: RevokeAPIKeyPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof revokeAPIKey>>,
TError,
{ pathParams: RevokeAPIKeyPathParameters },
TContext
> => {
const mutationOptions = getRevokeAPIKeyMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint updates an api key
* @summary Update api key
*/
export const updateAPIKey = (
{ id }: UpdateAPIKeyPathParameters,
typesStorableAPIKeyDTO: BodyType<TypesStorableAPIKeyDTO>,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/pats/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: typesStorableAPIKeyDTO,
});
};
export const getUpdateAPIKeyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateAPIKey>>,
TError,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateAPIKey>>,
TError,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
},
TContext
> => {
const mutationKey = ['updateAPIKey'];
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 updateAPIKey>>,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateAPIKey(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateAPIKeyMutationResult = NonNullable<
Awaited<ReturnType<typeof updateAPIKey>>
>;
export type UpdateAPIKeyMutationBody = BodyType<TypesStorableAPIKeyDTO>;
export type UpdateAPIKeyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update api key
*/
export const useUpdateAPIKey = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateAPIKey>>,
TError,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateAPIKey>>,
TError,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
},
TContext
> => {
const mutationOptions = getUpdateAPIKeyMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint resets the password by token
* @summary Reset password

View File

@@ -297,7 +297,11 @@ function CustomTimePicker({
resetErrorStatus();
};
const handleInputPressEnter = (): void => {
const handleInputPressEnter = (
event?: React.KeyboardEvent<HTMLInputElement>,
): void => {
event?.preventDefault();
event?.stopPropagation();
// check if the entered time is in the format of 1m, 2h, 3d, 4w
const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue);

View File

@@ -30,14 +30,15 @@ export default function CustomDomainEditModal({
onClearError,
onSubmit,
}: CustomDomainEditModalProps): JSX.Element {
const [value, setValue] = useState(customDomainSubdomain ?? '');
const initialSubdomain = customDomainSubdomain ?? '';
const [value, setValue] = useState(initialSubdomain);
const [validationError, setValidationError] = useState<string | null>(null);
useEffect(() => {
if (isOpen) {
setValue(customDomainSubdomain ?? '');
setValue(initialSubdomain);
}
}, [isOpen, customDomainSubdomain]);
}, [isOpen, initialSubdomain]);
const handleClose = (): void => {
setValidationError(null);
@@ -58,6 +59,11 @@ export default function CustomDomainEditModal({
};
const handleSubmit = (): void => {
if (value === initialSubdomain) {
setValidationError('Input is unchanged');
return;
}
if (!value) {
setValidationError('This field is required');
return;
@@ -84,7 +90,7 @@ export default function CustomDomainEditModal({
const hasError = Boolean(errorMessage);
const statusIcon = ((): JSX.Element => {
const statusIcon = ((): JSX.Element | null => {
if (isLoading) {
return (
<LoaderCircle size={16} className="animate-spin edit-modal-status-icon" />
@@ -95,7 +101,9 @@ export default function CustomDomainEditModal({
return <CircleAlert size={16} color={Color.BG_CHERRY_500} />;
}
return <CircleCheck size={16} color={Color.BG_FOREST_500} />;
return value && value.length >= 3 ? (
<CircleCheck size={16} color={Color.BG_FOREST_500} />
) : null;
})();
return (
@@ -189,7 +197,7 @@ export default function CustomDomainEditModal({
color="primary"
className="edit-modal-apply-btn"
onClick={handleSubmit}
disabled={isLoading}
disabled={isLoading || value === initialSubdomain}
loading={isLoading}
>
Apply Changes

View File

@@ -81,6 +81,10 @@
padding-left: 26px;
}
.custom-domain-card-meta-row.workspace-name-hidden {
padding-left: 0;
}
.custom-domain-card-meta-timezone {
display: inline-flex;
align-items: center;
@@ -117,32 +121,6 @@
background: var(--l2-border);
margin: 0;
}
.custom-domain-card-bottom {
display: flex;
align-items: center;
gap: var(--spacing-5);
padding: var(--padding-3);
}
.custom-domain-card-license {
color: var(--l1-foreground);
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
}
.custom-domain-plan-badge {
display: inline-flex;
align-items: center;
padding: 0 2px;
border-radius: 2px;
background: var(--l2-background);
color: var(--l2-foreground);
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
}
}
.workspace-url-trigger {

View File

@@ -69,8 +69,9 @@ function DomainUpdateToast({
}
export default function CustomDomainSettings(): JSX.Element {
const { org, activeLicense } = useAppContext();
const { org } = useAppContext();
const { timezone } = useTimezone();
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isPollingEnabled, setIsPollingEnabled] = useState(false);
const [hosts, setHosts] = useState<ZeustypesHostDTO[] | null>(null);
@@ -175,7 +176,8 @@ export default function CustomDomainSettings(): JSX.Element {
[hosts, activeHost],
);
const planName = activeLicense?.plan?.name;
const workspaceName =
org?.[0]?.displayName || customDomainSubdomain || activeHost?.name;
if (isLoadingHosts) {
return (
@@ -191,106 +193,98 @@ export default function CustomDomainSettings(): JSX.Element {
return (
<>
<div className="custom-domain-card">
<div className="custom-domain-card-top">
<div className="custom-domain-card-info">
<div className="custom-domain-card-top">
<div className="custom-domain-card-info">
{!!workspaceName && (
<div className="custom-domain-card-name-row">
<span className="beacon" />
<span className="custom-domain-card-org-name">
{org?.[0]?.displayName ? org?.[0]?.displayName : customDomainSubdomain}
</span>
<span className="custom-domain-card-org-name">{workspaceName}</span>
</div>
)}
<div className="custom-domain-card-meta-row">
<Dropdown
trigger={['click']}
dropdownRender={(): JSX.Element => (
<div className="workspace-url-dropdown">
<span className="workspace-url-dropdown-header">
All Workspace URLs
</span>
<div className="workspace-url-dropdown-divider" />
{sortedHosts.map((host) => {
const isActive = host.name === activeHost?.name;
return (
<a
key={host.name}
href={host.url}
target="_blank"
rel="noopener noreferrer"
className={`workspace-url-dropdown-item${
isActive ? ' workspace-url-dropdown-item--active' : ''
}`}
>
<span className="workspace-url-dropdown-item-label">
{stripProtocol(host.url ?? '')}
</span>
{isActive ? (
<Check size={14} className="workspace-url-dropdown-item-check" />
) : (
<ExternalLink
size={12}
className="workspace-url-dropdown-item-external"
/>
)}
</a>
);
})}
</div>
)}
>
<Button
type="button"
size="xs"
className="workspace-url-trigger"
disabled={isFetchingHosts}
>
<Link2 size={12} />
<span>{stripProtocol(activeHost?.url ?? '')}</span>
<ChevronDown size={12} />
</Button>
</Dropdown>
<span className="custom-domain-card-meta-timezone">
<Clock size={11} />
{timezone.offset}
</span>
</div>
</div>
<Button
variant="solid"
size="sm"
className="custom-domain-edit-button"
prefixIcon={<FilePenLine size={12} />}
disabled={isFetchingHosts || isPollingEnabled}
onClick={(): void => setIsEditModalOpen(true)}
<div
className={`custom-domain-card-meta-row ${
!workspaceName ? 'workspace-name-hidden' : ''
}`}
>
Edit workspace link
</Button>
<Dropdown
trigger={['click']}
dropdownRender={(): JSX.Element => (
<div className="workspace-url-dropdown">
<span className="workspace-url-dropdown-header">
All Workspace URLs
</span>
<div className="workspace-url-dropdown-divider" />
{sortedHosts.map((host) => {
const isActive = host.name === activeHost?.name;
return (
<a
key={host.name}
href={host.url}
target="_blank"
rel="noopener noreferrer"
className={`workspace-url-dropdown-item${
isActive ? ' workspace-url-dropdown-item--active' : ''
}`}
>
<span className="workspace-url-dropdown-item-label">
{stripProtocol(host.url ?? '')}
</span>
{isActive ? (
<Check size={14} className="workspace-url-dropdown-item-check" />
) : (
<ExternalLink
size={12}
className="workspace-url-dropdown-item-external"
/>
)}
</a>
);
})}
</div>
)}
>
<Button
type="button"
size="xs"
className="workspace-url-trigger"
disabled={isFetchingHosts}
>
<Link2 size={12} />
<span>{stripProtocol(activeHost?.url ?? '')}</span>
<ChevronDown size={12} />
</Button>
</Dropdown>
<span className="custom-domain-card-meta-timezone">
<Clock size={11} />
{timezone.offset}
</span>
</div>
</div>
{isPollingEnabled && (
<Callout
type="info"
showIcon
className="custom-domain-callout"
size="small"
icon={<SolidAlertCircle size={13} color="primary" />}
message={`Updating your URL to ⎯ ${customDomainSubdomain}.${dnsSuffix}. This may take a few mins.`}
/>
)}
<div className="custom-domain-card-divider" />
<div className="custom-domain-card-bottom">
<span className="beacon" />
<span className="custom-domain-card-license">
{planName && <code className="custom-domain-plan-badge">{planName}</code>}{' '}
license is currently active
</span>
</div>
<Button
variant="solid"
size="sm"
className="custom-domain-edit-button"
prefixIcon={<FilePenLine size={12} />}
disabled={isFetchingHosts || isPollingEnabled}
onClick={(): void => setIsEditModalOpen(true)}
>
Edit workspace link
</Button>
</div>
{isPollingEnabled && (
<Callout
type="info"
showIcon
className="custom-domain-callout"
size="small"
icon={<SolidAlertCircle size={13} color="primary" />}
message={`Updating your URL to ⎯ ${customDomainSubdomain}.${dnsSuffix}. This may take a few mins.`}
/>
)}
<CustomDomainEditModal
isOpen={isEditModalOpen}
onClose={(): void => setIsEditModalOpen(false)}

View File

@@ -239,4 +239,87 @@ describe('CustomDomainSettings', () => {
const { container } = render(toastRenderer('test-id'));
expect(container).toHaveTextContent(/myteam\.test\.cloud/i);
});
describe('Workspace Name rendering', () => {
it('renders org displayName when available from appContext', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
render(<CustomDomainSettings />, undefined, {
appContextOverrides: {
org: [{ id: 'xyz', displayName: 'My Org Name', createdAt: 0 }],
},
});
expect(await screen.findByText('My Org Name')).toBeInTheDocument();
});
it('falls back to customDomainSubdomain when org displayName is missing', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
render(<CustomDomainSettings />, undefined, {
appContextOverrides: { org: [] },
});
expect(await screen.findByText('custom-host')).toBeInTheDocument();
});
it('falls back to activeHost.name when neither org name nor custom domain exists', async () => {
const onlyDefaultHostResponse = {
...mockHostsResponse,
data: {
...mockHostsResponse.data,
hosts: mockHostsResponse.data.hosts
? [mockHostsResponse.data.hosts[0]]
: [],
},
};
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(onlyDefaultHostResponse)),
),
);
render(<CustomDomainSettings />, undefined, {
appContextOverrides: { org: [] },
});
// 'accepted-starfish' is the default host's name
expect(await screen.findByText('accepted-starfish')).toBeInTheDocument();
});
it('does not render the card name row if workspaceName is totally falsy', async () => {
const emptyHostsResponse = {
...mockHostsResponse,
data: {
...mockHostsResponse.data,
hosts: [],
},
};
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(emptyHostsResponse)),
),
);
const { container } = render(<CustomDomainSettings />, undefined, {
appContextOverrides: { org: [] },
});
await screen.findByRole('button', { name: /edit workspace link/i });
expect(
container.querySelector('.custom-domain-card-name-row'),
).not.toBeInTheDocument();
});
});
});

View File

@@ -34,11 +34,6 @@ const mockSafeNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
useRouteMatch: jest.fn().mockReturnValue({
params: {
dashboardId: 4,
},
}),
}));
jest.mock(
@@ -69,7 +64,7 @@ describe('Dashboard landing page actions header tests', () => {
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const { getByTestId } = render(
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
<DashboardProvider>
<DashboardProvider dashboardId="4">
<DashboardDescription
handle={{
active: false,
@@ -110,7 +105,7 @@ describe('Dashboard landing page actions header tests', () => {
);
const { getByTestId } = render(
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
<DashboardProvider>
<DashboardProvider dashboardId="4">
<DashboardDescription
handle={{
active: false,
@@ -149,7 +144,7 @@ describe('Dashboard landing page actions header tests', () => {
const { getByText } = render(
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
<DashboardProvider>
<DashboardProvider dashboardId="4">
<DashboardDescription
handle={{
active: false,
@@ -199,8 +194,6 @@ describe('Dashboard landing page actions header tests', () => {
setLayouts: jest.fn(),
setSelectedDashboard: jest.fn(),
updatedTimeRef: { current: null },
toScrollWidgetId: '',
setToScrollWidgetId: jest.fn(),
updateLocalStorageDashboardVariables: jest.fn(),
dashboardQueryRangeCalled: false,
setDashboardQueryRangeCalled: jest.fn(),

View File

@@ -9,7 +9,6 @@ import {
} from 'hooks/dashboard/useDashboardVariables';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import {
enqueueDescendantsOfVariable,
@@ -30,7 +29,7 @@ function DashboardVariableSelection(): JSX.Element | null {
updateLocalStorageDashboardVariables,
} = useDashboard();
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
const { updateUrlVariable } = useVariablesFromUrl();
const { dashboardVariables } = useDashboardVariables();
const dashboardId = useDashboardVariablesSelector(
@@ -50,15 +49,6 @@ function DashboardVariableSelection(): JSX.Element | null {
(state) => state.globalTime,
);
useEffect(() => {
// Initialize variables with default values if not in URL
initializeDefaultVariables(
dashboardVariables,
getUrlVariables,
updateUrlVariable,
);
}, [getUrlVariables, updateUrlVariable, dashboardVariables]);
// Memoize the order key to avoid unnecessary triggers
const variableOrderKey = useMemo(() => {
const queryVariableOrderKey = dependencyData?.order?.join(',') ?? '';

View File

@@ -1,9 +1,9 @@
import { renderHook } from '@testing-library/react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useScrollToWidgetIdStore } from 'providers/Dashboard/helpers/scrollToWidgetIdHelper';
import { useScrollWidgetIntoView } from '../useScrollWidgetIntoView';
jest.mock('providers/Dashboard/Dashboard');
jest.mock('providers/Dashboard/helpers/scrollToWidgetIdHelper');
type MockHTMLElement = {
scrollIntoView: jest.Mock;
@@ -18,25 +18,35 @@ function createMockElement(): MockHTMLElement {
}
describe('useScrollWidgetIntoView', () => {
const mockedUseDashboard = useDashboard as jest.MockedFunction<
typeof useDashboard
const mockedUseScrollToWidgetIdStore = useScrollToWidgetIdStore as jest.MockedFunction<
typeof useScrollToWidgetIdStore
>;
let mockElement: MockHTMLElement;
let ref: React.RefObject<HTMLDivElement>;
let setToScrollWidgetId: jest.Mock;
function mockStore(toScrollWidgetId: string): void {
const storeState = { toScrollWidgetId, setToScrollWidgetId };
mockedUseScrollToWidgetIdStore.mockImplementation(
(selector) =>
selector(
(storeState as unknown) as Parameters<typeof selector>[0],
) as ReturnType<typeof useScrollToWidgetIdStore>,
);
}
beforeEach(() => {
jest.clearAllMocks();
mockElement = createMockElement();
ref = ({
current: mockElement,
} as unknown) as React.RefObject<HTMLDivElement>;
setToScrollWidgetId = jest.fn();
});
it('scrolls into view and focuses when toScrollWidgetId matches widget id', () => {
const setToScrollWidgetId = jest.fn();
const mockElement = createMockElement();
const ref = ({
current: mockElement,
} as unknown) as React.RefObject<HTMLDivElement>;
mockedUseDashboard.mockReturnValue(({
toScrollWidgetId: 'widget-id',
setToScrollWidgetId,
} as unknown) as ReturnType<typeof useDashboard>);
mockStore('widget-id');
renderHook(() => useScrollWidgetIntoView('widget-id', ref));
@@ -49,16 +59,7 @@ describe('useScrollWidgetIntoView', () => {
});
it('does nothing when toScrollWidgetId does not match widget id', () => {
const setToScrollWidgetId = jest.fn();
const mockElement = createMockElement();
const ref = ({
current: mockElement,
} as unknown) as React.RefObject<HTMLDivElement>;
mockedUseDashboard.mockReturnValue(({
toScrollWidgetId: 'other-widget',
setToScrollWidgetId,
} as unknown) as ReturnType<typeof useDashboard>);
mockStore('other-widget');
renderHook(() => useScrollWidgetIntoView('widget-id', ref));

View File

@@ -1,5 +1,5 @@
import { RefObject, useEffect } from 'react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useScrollToWidgetIdStore } from 'providers/Dashboard/helpers/scrollToWidgetIdHelper';
/**
* Scrolls the given widget container into view when the dashboard
@@ -11,7 +11,10 @@ export function useScrollWidgetIntoView<T extends HTMLElement>(
widgetId: string,
widgetContainerRef: RefObject<T>,
): void {
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const toScrollWidgetId = useScrollToWidgetIdStore((s) => s.toScrollWidgetId);
const setToScrollWidgetId = useScrollToWidgetIdStore(
(s) => s.setToScrollWidgetId,
);
useEffect(() => {
if (toScrollWidgetId === widgetId) {

View File

@@ -1,5 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useScrollWidgetIntoView } from 'container/DashboardContainer/visualization/hooks/useScrollWidgetIntoView';
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
@@ -34,8 +33,6 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const { timezone } = useTimezone();
useScrollWidgetIntoView(widget.id, graphRef);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(queryResponse);

View File

@@ -1,5 +1,4 @@
import { useMemo, useRef } from 'react';
import { useScrollWidgetIntoView } from 'container/DashboardContainer/visualization/hooks/useScrollWidgetIntoView';
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
@@ -32,8 +31,6 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const { timezone } = useTimezone();
useScrollWidgetIntoView(widget.id, graphRef);
const config = useMemo(() => {
return prepareHistogramPanelConfig({
widget,

View File

@@ -2,7 +2,6 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
import ChartManager from 'container/DashboardContainer/visualization/components/ChartManager/ChartManager';
import { usePanelContextMenu } from 'container/DashboardContainer/visualization/hooks/usePanelContextMenu';
import { useScrollWidgetIntoView } from 'container/DashboardContainer/visualization/hooks/useScrollWidgetIntoView';
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
@@ -33,8 +32,6 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const { timezone } = useTimezone();
useScrollWidgetIntoView(widget.id, graphRef);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(queryResponse);

View File

@@ -10,6 +10,7 @@ import setRetentionApi from 'api/settings/setRetention';
import setRetentionApiV2 from 'api/settings/setRetentionV2';
import TextToolTip from 'components/TextToolTip';
import CustomDomainSettings from 'container/CustomDomainSettings';
import LicenseKeyRow from 'container/GeneralSettings/LicenseKeyRow/LicenseKeyRow';
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
import useComponentPermission from 'hooks/useComponentPermission';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
@@ -81,7 +82,7 @@ function GeneralSettings({
logsTtlValuesPayload,
);
const { user } = useAppContext();
const { user, activeLicense } = useAppContext();
const [setRetentionPermission] = useComponentPermission(
['set_retention_period'],
@@ -680,7 +681,15 @@ function GeneralSettings({
</span>
</div>
{showCustomDomainSettings && <CustomDomainSettings />}
{(showCustomDomainSettings || activeLicense?.key) && (
<div className="custom-domain-card">
{showCustomDomainSettings && <CustomDomainSettings />}
{showCustomDomainSettings && activeLicense?.key && (
<div className="custom-domain-card-divider" />
)}
{activeLicense?.key && <LicenseKeyRow />}
</div>
)}
<div className="retention-controls-container">
<div className="retention-controls-header">

View File

@@ -0,0 +1,65 @@
.license-key-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--padding-2) var(--padding-3);
gap: var(--spacing-5);
&__left {
display: inline-flex;
align-items: center;
gap: 12px;
color: var(--l2-foreground);
svg {
flex-shrink: 0;
}
}
&__label {
color: var(--l2-foreground);
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
flex-shrink: 0;
}
&__value {
display: inline-flex;
align-items: stretch;
}
&__code {
display: inline-flex;
align-items: center;
padding: 1px 2px;
border-radius: 2px 0 0 2px;
background: var(--l3-background);
border: 1px solid var(--l2-border);
color: var(--l2-foreground);
font-family: 'SF Mono', 'Fira Code', 'Fira Mono', monospace;
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
white-space: nowrap;
margin-right: -1px;
}
&__copy-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
padding: 1px 2px;
border-radius: 0 2px 2px 0;
background: var(--l3-background);
border: 1px solid var(--l2-border);
color: var(--l2-foreground);
cursor: pointer;
flex-shrink: 0;
height: 24px;
&:hover {
background: var(--l3-background-hover);
}
}
}

View File

@@ -0,0 +1,48 @@
import { useCopyToClipboard } from 'react-use';
import { Button } from '@signozhq/button';
import { Copy, KeyRound } from '@signozhq/icons';
import { toast } from '@signozhq/sonner';
import { useAppContext } from 'providers/App/App';
import { getMaskedKey } from 'utils/maskedKey';
import './LicenseKeyRow.styles.scss';
function LicenseKeyRow(): JSX.Element | null {
const { activeLicense } = useAppContext();
const [, copyToClipboard] = useCopyToClipboard();
if (!activeLicense?.key) {
return null;
}
const handleCopyLicenseKey = (text: string): void => {
copyToClipboard(text);
toast.success('License key copied to clipboard.', { richColors: true });
};
return (
<div className="license-key-row">
<span className="license-key-row__left">
<KeyRound size={14} />
<span className="license-key-row__label">SigNoz License Key</span>
</span>
<span className="license-key-row__value">
<code className="license-key-row__code">
{getMaskedKey(activeLicense.key)}
</code>
<Button
type="button"
size="xs"
aria-label="Copy license key"
data-testid="license-key-row-copy-btn"
className="license-key-row__copy-btn"
onClick={(): void => handleCopyLicenseKey(activeLicense.key)}
>
<Copy size={12} />
</Button>
</span>
</div>
);
}
export default LicenseKeyRow;

View File

@@ -0,0 +1,61 @@
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import LicenseKeyRow from '../LicenseKeyRow';
const mockCopyToClipboard = jest.fn();
jest.mock('react-use', () => ({
__esModule: true,
useCopyToClipboard: (): [unknown, jest.Mock] => [null, mockCopyToClipboard],
}));
const mockToastSuccess = jest.fn();
jest.mock('@signozhq/sonner', () => ({
toast: {
success: (...args: unknown[]): unknown => mockToastSuccess(...args),
},
}));
describe('LicenseKeyRow', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('renders nothing when activeLicense key is absent', () => {
const { container } = render(<LicenseKeyRow />, undefined, {
appContextOverrides: { activeLicense: null },
});
expect(container).toBeEmptyDOMElement();
});
it('renders label and masked key when activeLicense key exists', () => {
render(<LicenseKeyRow />, undefined, {
appContextOverrides: {
activeLicense: { key: 'abcdefghij' } as any,
},
});
expect(screen.getByText('SigNoz License Key')).toBeInTheDocument();
expect(screen.getByText('ab·······ij')).toBeInTheDocument();
});
it('calls copyToClipboard and shows success toast when clipboard is available', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<LicenseKeyRow />);
await user.click(screen.getByRole('button', { name: /copy license key/i }));
await waitFor(() => {
expect(mockCopyToClipboard).toHaveBeenCalledWith('test-key');
expect(mockToastSuccess).toHaveBeenCalledWith(
'License key copied to clipboard.',
{
richColors: true,
},
);
});
});
});

View File

@@ -5,6 +5,7 @@ import logEvent from 'api/common/logEvent';
import { DEFAULT_ENTITY_VERSION, ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useScrollWidgetIntoView } from 'container/DashboardContainer/visualization/hooks/useScrollWidgetIntoView';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsPanelWaitingOnVariable } from 'hooks/dashboard/useVariableFetchState';
@@ -67,11 +68,7 @@ function GridCardGraph({
const [isInternalServerError, setIsInternalServerError] = useState<boolean>(
false,
);
const {
toScrollWidgetId,
setToScrollWidgetId,
setDashboardQueryRangeCalled,
} = useDashboard();
const { setDashboardQueryRangeCalled } = useDashboard();
const {
minTime,
@@ -109,20 +106,11 @@ function GridCardGraph({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const graphRef = useRef<HTMLDivElement>(null);
const widgetContainerRef = useRef<HTMLDivElement>(null);
const isVisible = useIntersectionObserver(graphRef, undefined, true);
const isVisible = useIntersectionObserver(widgetContainerRef, undefined, true);
useEffect(() => {
if (toScrollWidgetId === widget.id) {
graphRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
graphRef.current?.focus();
setToScrollWidgetId('');
}
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
useScrollWidgetIntoView(widget?.id || '', widgetContainerRef);
const updatedQuery = widget?.query;
@@ -306,7 +294,7 @@ function GridCardGraph({
: headerMenuList;
return (
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
<div style={{ height: '100%', width: '100%' }} ref={widgetContainerRef}>
{isEmptyLayout ? (
<EmptyWidget />
) : (

View File

@@ -5,7 +5,6 @@ import NewWidget from 'container/NewWidget';
import { logsPaginationQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import i18n from 'ReactI18';
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
@@ -104,15 +103,13 @@ describe('LogsPanelComponent', () => {
const renderComponent = async (): Promise<void> => {
render(
<I18nextProvider i18n={i18n}>
<DashboardProvider>
<PreferenceContextProvider>
<NewWidget
selectedGraph={PANEL_TYPES.LIST}
fillSpans={undefined}
yAxisUnit={undefined}
/>
</PreferenceContextProvider>
</DashboardProvider>
<PreferenceContextProvider>
<NewWidget
dashboardId=""
selectedDashboard={undefined}
selectedGraph={PANEL_TYPES.LIST}
/>
</PreferenceContextProvider>
</I18nextProvider>,
);

View File

@@ -4,6 +4,7 @@ import { Typography } from 'antd';
import { useNotifications } from 'hooks/useNotifications';
import { Copy } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { getMaskedKey } from 'utils/maskedKey';
import './LicenseSection.styles.scss';
@@ -12,15 +13,6 @@ function LicenseSection(): JSX.Element | null {
const { notifications } = useNotifications();
const [, handleCopyToClipboard] = useCopyToClipboard();
const getMaskedKey = (key: string): string => {
if (!key || key.length < 4) {
return key || 'N/A';
}
return `${key.substring(0, 2)}********${key
.substring(key.length - 2)
.trim()}`;
};
const handleCopyKey = (text: string): void => {
handleCopyToClipboard(text);
notifications.success({

View File

@@ -271,7 +271,7 @@ describe('MySettings Flows', () => {
},
});
expect(within(container).getByText('ab********cd')).toBeInTheDocument();
expect(within(container).getByText('ab·······cd')).toBeInTheDocument();
});
it('Should not mask license key if it is too short', () => {

View File

@@ -8,28 +8,15 @@ import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import TextToolTip from 'components/TextToolTip';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
import {
getDefaultWidgetData,
PANEL_TYPE_TO_QUERY_TYPES,
} from 'container/NewWidget/utils';
import { PANEL_TYPE_TO_QUERY_TYPES } from 'container/NewWidget/utils';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
// import { QueryBuilder } from 'container/QueryBuilder';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { defaultTo, isUndefined } from 'lodash-es';
import { Atom, Terminal } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
getNextWidgets,
getPreviousWidgets,
getSelectedWidgetIndex,
} from 'providers/Dashboard/util';
import { Widgets } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
@@ -40,77 +27,25 @@ function QuerySection({
selectedGraph,
queryRangeKey,
isLoadingQueries,
selectedWidget,
dashboardVersion,
dashboardId,
dashboardName,
isNewPanel,
}: QueryProps): JSX.Element {
const {
currentQuery,
handleRunQuery: handleRunQueryFromQueryBuilder,
redirectWithQueryBuilderData,
} = useQueryBuilder();
const urlQuery = useUrlQuery();
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
const { selectedDashboard, setSelectedDashboard } = useDashboard();
const isDarkMode = useIsDarkMode();
const { widgets } = selectedDashboard?.data || {};
const getWidget = useCallback(() => {
const widgetId = urlQuery.get('widgetId');
return defaultTo(
widgets?.find((e) => e.id === widgetId),
getDefaultWidgetData(widgetId || '', selectedGraph),
);
}, [urlQuery, widgets, selectedGraph]);
const selectedWidget = getWidget() as Widgets;
const { query } = selectedWidget;
useShareBuilderUrl({ defaultValue: query });
const handleStageQuery = useCallback(
(query: Query): void => {
if (selectedDashboard === undefined) {
return;
}
const selectedWidgetIndex = getSelectedWidgetIndex(
selectedDashboard,
selectedWidget.id,
);
const previousWidgets = getPreviousWidgets(
selectedDashboard,
selectedWidgetIndex,
);
const nextWidgets = getNextWidgets(selectedDashboard, selectedWidgetIndex);
setSelectedDashboard({
...selectedDashboard,
data: {
...selectedDashboard?.data,
widgets: [
...previousWidgets,
{
...selectedWidget,
query,
},
...nextWidgets,
],
},
});
handleRunQueryFromQueryBuilder();
},
[
selectedDashboard,
selectedWidget,
setSelectedDashboard,
handleRunQueryFromQueryBuilder,
],
);
const handleQueryCategoryChange = useCallback(
(qCategory: string): void => {
const currentQueryType = qCategory as EQueryType;
@@ -123,19 +58,16 @@ function QuerySection({
);
const handleRunQuery = (): void => {
const widgetId = urlQuery.get('widgetId');
const isNewPanel = isUndefined(widgets?.find((e) => e.id === widgetId));
logEvent('Panel Edit: Stage and run query', {
dataSource: currentQuery.builder?.queryData?.[0]?.dataSource,
panelType: selectedWidget.panelTypes,
queryType: currentQuery.queryType,
widgetId: selectedWidget.id,
dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title,
dashboardId,
dashboardName,
isNewPanel,
});
handleStageQuery(currentQuery);
handleRunQueryFromQueryBuilder();
};
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
@@ -164,7 +96,7 @@ function QuerySection({
panelType={selectedGraph}
filterConfigs={filterConfigs}
showTraceOperator={selectedGraph !== PANEL_TYPES.LIST}
version={selectedDashboard?.data?.version || 'v3'}
version={dashboardVersion || 'v3'}
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
queryComponents={queryComponents}
signalSourceChangeEnabled
@@ -204,7 +136,7 @@ function QuerySection({
queryComponents,
selectedGraph,
filterConfigs,
selectedDashboard?.data?.version,
dashboardVersion,
isDarkMode,
]);
@@ -261,6 +193,11 @@ interface QueryProps {
selectedGraph: PANEL_TYPES;
queryRangeKey?: QueryKey;
isLoadingQueries?: boolean;
selectedWidget: Widgets;
dashboardVersion?: string;
dashboardId?: string;
dashboardName?: string;
isNewPanel?: boolean;
}
export default QuerySection;

View File

@@ -30,6 +30,8 @@ function LeftContainer({
setRequestData,
setQueryResponse,
enableDrillDown = false,
selectedDashboard,
isNewPanel = false,
}: WidgetGraphProps): JSX.Element {
const { stagedQuery } = useQueryBuilder();
@@ -75,6 +77,11 @@ function LeftContainer({
selectedGraph={selectedGraph}
queryRangeKey={queryRangeKey}
isLoadingQueries={queryResponse.isFetching}
selectedWidget={selectedWidget}
dashboardVersion={ENTITY_VERSION_V5}
dashboardId={selectedDashboard?.id}
dashboardName={selectedDashboard?.data.title}
isNewPanel={isNewPanel}
/>
{selectedGraph === PANEL_TYPES.LIST && (
<ExplorerColumnsRenderer

View File

@@ -1,5 +1,7 @@
.column-unit-selector {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 8px;
.heading {
color: var(--bg-vanilla-400);
@@ -30,6 +32,11 @@
width: 100%;
}
}
&-content {
display: flex;
flex-direction: column;
gap: 12px;
}
}
.lightMode {

View File

@@ -72,22 +72,24 @@ export function ColumnUnitSelector(
return (
<section className="column-unit-selector">
<Typography.Text className="heading">Column Units</Typography.Text>
{aggregationQueries.map(({ value, label }) => {
const baseQueryName = value.split('.')[0];
return (
<YAxisUnitSelectorV2
value={columnUnits[value] || ''}
onSelect={(unitValue: string): void =>
handleColumnUnitSelect(value, unitValue)
}
fieldLabel={label}
key={value}
selectedQueryName={baseQueryName}
// Update the column unit value automatically only in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/>
);
})}
<div className="column-unit-selector-content">
{aggregationQueries.map(({ value, label }) => {
const baseQueryName = value.split('.')[0];
return (
<YAxisUnitSelectorV2
value={columnUnits[value] || ''}
onSelect={(unitValue: string): void =>
handleColumnUnitSelect(value, unitValue)
}
fieldLabel={label}
key={value}
selectedQueryName={baseQueryName}
// Update the column unit value automatically only in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/>
);
})}
</div>
</section>
);
}

View File

@@ -56,9 +56,6 @@ describe('ContextLinks Component', () => {
/>,
);
// Check that the component renders
expect(screen.getByText('Context Links')).toBeInTheDocument();
// Check that the add button is present
expect(
screen.getByRole('button', { name: /context link/i }),

View File

@@ -14,7 +14,7 @@ import {
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button, Modal, Typography } from 'antd';
import { Button, Modal } from 'antd';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react';
import {
@@ -134,11 +134,16 @@ function ContextLinks({
return (
<div className="context-links-container">
<Typography.Text className="context-links-text">
Context Links
</Typography.Text>
<div className="context-links-list">
<Button
type="default"
className="add-context-link-button"
icon={<Plus size={12} />}
style={{ width: '100%' }}
onClick={handleAddContextLink}
>
Add Context Link
</Button>
<OverlayScrollbar>
<DndContext
sensors={sensors}
@@ -160,16 +165,6 @@ function ContextLinks({
</SortableContext>
</DndContext>
</OverlayScrollbar>
{/* button to add context link */}
<Button
type="primary"
className="add-context-link-button"
icon={<Plus size={12} />}
onClick={handleAddContextLink}
>
Context Link
</Button>
</div>
<Modal

View File

@@ -2,7 +2,6 @@
display: flex;
flex-direction: column;
gap: 16px;
margin: 12px;
}
.context-links-text {
@@ -110,10 +109,7 @@
}
.add-context-link-button {
display: flex;
align-items: center;
margin: auto;
width: fit-content;
width: 100%;
}
.lightMode {

View File

@@ -24,14 +24,14 @@
letter-spacing: -0.07px;
}
}
.name-description {
.control-container {
display: flex;
flex-direction: column;
padding: 12px 12px 16px 12px;
border-top: 1px solid var(--bg-slate-500);
border-bottom: 1px solid var(--bg-slate-500);
gap: 8px;
}
.name-description {
padding: 0 0 4px 0;
.typography {
color: var(--bg-vanilla-400);
@@ -88,9 +88,6 @@
.panel-config {
display: flex;
flex-direction: column;
padding: 12px 12px 16px 12px;
gap: 8px;
border-bottom: 1px solid var(--bg-slate-500);
.typography {
color: var(--bg-vanilla-400);
@@ -104,6 +101,7 @@
}
.panel-type-select {
width: 100%;
.ant-select-selector {
display: flex;
height: 32px;
@@ -137,7 +135,6 @@
}
.fill-gaps {
margin-top: 16px;
display: flex;
padding: 12px;
justify-content: space-between;
@@ -156,31 +153,24 @@
letter-spacing: 0.52px;
text-transform: uppercase;
}
.fill-gaps-text-description {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
opacity: 0.6;
line-height: 16px; /* 133.333% */
}
}
.log-scale,
.decimal-precision-selector {
margin-top: 16px;
display: flex;
justify-content: space-between;
flex-direction: column;
gap: 8px;
}
.decimal-precision-selector,
.legend-position {
margin-top: 16px;
display: flex;
justify-content: space-between;
flex-direction: column;
gap: 8px;
}
.legend-colors {
margin-top: 16px;
}
.panel-time-text {
margin-top: 16px;
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
@@ -193,7 +183,6 @@
.y-axis-unit-selector,
.y-axis-unit-selector-v2 {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 8px;
@@ -278,11 +267,8 @@
}
.stack-chart {
margin-top: 16px;
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 8px;
.label {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
@@ -296,11 +282,6 @@
}
.bucket-config {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 8px;
.label {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
@@ -352,16 +333,13 @@
}
}
.context-links {
border-bottom: 1px solid var(--bg-slate-500);
}
.alerts {
display: flex;
padding: 12px;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--bg-slate-500);
padding: 12px;
min-height: 44px;
border-top: 1px solid var(--bg-slate-500);
cursor: pointer;
.left-section {
@@ -387,6 +365,16 @@
color: var(--bg-vanilla-400);
}
}
.context-links {
padding: 12px 12px 16px 12px;
border-bottom: 1px solid var(--bg-slate-500);
}
.thresholds-section {
padding: 12px 12px 16px 12px;
border-top: 1px solid var(--bg-slate-500);
}
}
.select-option {
@@ -418,9 +406,6 @@
}
.name-description {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.typography {
color: var(--bg-ink-400);
}
@@ -441,8 +426,6 @@
}
.panel-config {
border-bottom: 1px solid var(--bg-vanilla-300);
.typography {
color: var(--bg-ink-400);
}
@@ -478,6 +461,9 @@
.fill-gaps-text {
color: var(--bg-ink-400);
}
.fill-gaps-text-description {
color: var(--bg-ink-400);
}
}
.bucket-config {
@@ -530,7 +516,7 @@
}
.alerts {
border-bottom: 1px solid var(--bg-vanilla-300);
border-top: 1px solid var(--bg-vanilla-300);
.left-section {
.bell-icon {
@@ -549,6 +535,10 @@
.context-links {
border-bottom: 1px solid var(--bg-vanilla-300);
}
.thresholds-section {
border-top: 1px solid var(--bg-vanilla-300);
}
}
.select-option {

View File

@@ -1,5 +1,4 @@
.threshold-selector-container {
padding: 12px;
padding-bottom: 80px;
.threshold-select {

View File

@@ -1,10 +1,10 @@
import { useCallback } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { Typography } from 'antd';
import { Button } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
import { Antenna, Plus } from 'lucide-react';
import { Plus } from 'lucide-react';
import { v4 as uuid } from 'uuid';
import Threshold from './Threshold';
@@ -68,11 +68,14 @@ function ThresholdSelector({
<DndProvider backend={HTML5Backend}>
<div className="threshold-selector-container">
<div className="threshold-select" onClick={addThresholdHandler}>
<div className="left-section">
<Antenna size={14} className="icon" />
<Typography.Text className="text">Thresholds</Typography.Text>
</div>
<Plus size={14} onClick={addThresholdHandler} className="icon" />
<Button
type="default"
icon={<Plus size={14} />}
style={{ width: '100%' }}
onClick={addThresholdHandler}
>
Add Threshold
</Button>
</div>
{thresholds.map((threshold, idx) => (
<Threshold

View File

@@ -8,7 +8,6 @@ import userEvent from '@testing-library/user-event';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { AppContext } from 'providers/App/App';
import { IAppContext } from 'providers/App/types';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import configureStore from 'redux-mock-store';
@@ -96,9 +95,7 @@ const render = (ui: React.ReactElement): ReturnType<typeof rtlRender> =>
<Provider store={createMockStore()}>
<AppContext.Provider value={createMockAppContext() as IAppContext}>
<ErrorModalProvider>
<DashboardProvider>
<QueryBuilderProvider>{ui}</QueryBuilderProvider>
</DashboardProvider>
<QueryBuilderProvider>{ui}</QueryBuilderProvider>
</ErrorModalProvider>
</AppContext.Provider>
</Provider>

View File

@@ -0,0 +1,68 @@
.settings-section {
border-top: 1px solid var(--bg-slate-500);
}
.settings-section-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 12px 12px;
min-height: 44px;
background: transparent;
border: none;
outline: none;
cursor: pointer;
.settings-section-title {
display: flex;
align-items: center;
gap: 8px;
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
font-weight: 400;
text-transform: uppercase;
}
.chevron-icon {
color: var(--bg-vanilla-400);
transition: transform 0.2s ease-in-out;
&.open {
transform: rotate(180deg);
}
}
}
.settings-section-content {
padding: 0 12px 0 12px;
display: flex;
flex-direction: column;
gap: 20px;
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 0.25s ease, opacity 0.25s ease, padding 0.25s ease;
&.open {
padding-bottom: 24px;
max-height: 1000px;
opacity: 1;
}
}
.lightMode {
.settings-section-header {
.chevron-icon {
color: var(--bg-ink-400);
}
.settings-section-title {
color: var(--bg-ink-400);
}
}
.settings-section {
border-top: 1px solid var(--bg-vanilla-300);
}
}

View File

@@ -0,0 +1,51 @@
import { ReactNode, useState } from 'react';
import { ChevronDown } from 'lucide-react';
import './SettingsSection.styles.scss';
export interface SettingsSectionProps {
title: string;
defaultOpen?: boolean;
children: ReactNode;
icon?: ReactNode;
}
function SettingsSection({
title,
defaultOpen = false,
children,
icon,
}: SettingsSectionProps): JSX.Element {
const [isOpen, setIsOpen] = useState(defaultOpen);
const toggleOpen = (): void => {
setIsOpen((prev) => !prev);
};
return (
<section className="settings-section">
<button
type="button"
className="settings-section-header"
onClick={toggleOpen}
>
<span className="settings-section-title">
{icon ? icon : null} {title}
</span>
<ChevronDown
size={16}
className={isOpen ? 'chevron-icon open' : 'chevron-icon'}
/>
</button>
<div
className={
isOpen ? 'settings-section-content open' : 'settings-section-content'
}
>
{children}
</div>
</section>
);
}
export default SettingsSection;

View File

@@ -14,7 +14,6 @@ import {
Input,
InputNumber,
Select,
Space,
Switch,
Typography,
} from 'antd';
@@ -28,9 +27,16 @@ import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import {
Antenna,
Axis3D,
ConciergeBell,
Layers,
LayoutDashboard,
LineChart,
Link,
Pencil,
Plus,
SlidersHorizontal,
Spline,
SquareArrowOutUpRight,
} from 'lucide-react';
@@ -46,6 +52,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { popupContainer } from 'utils/selectPopupContainer';
import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector';
import SettingsSection from './components/SettingsSection/SettingsSection';
import {
panelTypeVsBucketConfig,
panelTypeVsColumnUnitPreferences,
@@ -178,6 +185,21 @@ function RightContainer({
}));
}, [dashboardVariables]);
const isAxisSectionVisible = useMemo(() => allowSoftMinMax || allowLogScale, [
allowSoftMinMax,
allowLogScale,
]);
const isFormattingSectionVisible = useMemo(
() => allowYAxisUnit || allowDecimalPrecision || allowPanelColumnPreference,
[allowYAxisUnit, allowDecimalPrecision, allowPanelColumnPreference],
);
const isLegendSectionVisible = useMemo(
() => allowLegendPosition || allowLegendColors,
[allowLegendPosition, allowLegendColors],
);
const updateCursorAndDropdown = (value: string, pos: number): void => {
setCursorPos(pos);
const lastDollar = value.lastIndexOf('$', pos - 1);
@@ -193,6 +215,15 @@ function RightContainer({
}, 0);
};
const decimapPrecisionOptions = useMemo(() => {
return [
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
];
}, []);
const handleInputCursor = (): void => {
const pos = inputRef.current?.input?.selectionStart ?? 0;
updateCursorAndDropdown(inputValue, pos);
@@ -263,269 +294,297 @@ function RightContainer({
<div className="right-container">
<section className="header">
<div className="purple-dot" />
<Typography.Text className="header-text">Panel details</Typography.Text>
<Typography.Text className="header-text">Panel Settings</Typography.Text>
</section>
<section className="name-description">
<Typography.Text className="typography">Name</Typography.Text>
<AutoComplete
options={dashboardVariableOptions}
value={inputValue}
onChange={onInputChange}
onSelect={onSelect}
filterOption={filterOption}
style={{ width: '100%' }}
getPopupContainer={popupContainer}
placeholder="Enter the panel name here..."
open={autoCompleteOpen}
>
<Input
rootClassName="name-input"
ref={inputRef}
onSelect={handleInputCursor}
onClick={handleInputCursor}
onBlur={(): void => setAutoCompleteOpen(false)}
/>
</AutoComplete>
<Typography.Text className="typography">Description</Typography.Text>
<TextArea
placeholder="Enter the panel description here..."
bordered
allowClear
value={description}
onChange={(event): void =>
onChangeHandler(setDescription, event.target.value)
}
rootClassName="description-input"
/>
</section>
<section className="panel-config">
<Typography.Text className="typography">Panel Type</Typography.Text>
<Select
onChange={setGraphHandler}
value={selectedGraph}
style={{ width: '100%' }}
className="panel-type-select"
data-testid="panel-change-select"
data-stacking-state={stackedBarChart ? 'true' : 'false'}
>
{graphTypes.map((item) => (
<Option key={item.name} value={item.name}>
<div className="select-option">
<div className="icon">{item.icon}</div>
<Typography.Text className="display">{item.display}</Typography.Text>
</div>
</Option>
))}
</Select>
{allowFillSpans && (
<Space className="fill-gaps">
<Typography className="fill-gaps-text">Fill gaps</Typography>
<Switch
checked={isFillSpans}
size="small"
onChange={(checked): void => setIsFillSpans(checked)}
<SettingsSection title="General" defaultOpen icon={<Pencil size={14} />}>
<section className="name-description control-container">
<Typography.Text className="typography">Name</Typography.Text>
<AutoComplete
options={dashboardVariableOptions}
value={inputValue}
onChange={onInputChange}
onSelect={onSelect}
filterOption={filterOption}
style={{ width: '100%' }}
getPopupContainer={popupContainer}
placeholder="Enter the panel name here..."
open={autoCompleteOpen}
>
<Input
rootClassName="name-input"
ref={inputRef}
onSelect={handleInputCursor}
onClick={handleInputCursor}
onBlur={(): void => setAutoCompleteOpen(false)}
/>
</Space>
)}
{allowPanelTimePreference && (
<>
<Typography.Text className="panel-time-text">
Panel Time Preference
</Typography.Text>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
/>
</>
)}
{allowPanelColumnPreference && (
<ColumnUnitSelector
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
isNewDashboard={isNewDashboard}
/>
)}
{allowYAxisUnit && (
<DashboardYAxisUnitSelectorWrapper
onSelect={setYAxisUnit}
value={yAxisUnit || ''}
fieldLabel={
selectedGraphType === PanelDisplay.VALUE ||
selectedGraphType === PanelDisplay.PIE
? 'Unit'
: 'Y Axis Unit'
</AutoComplete>
<Typography.Text className="typography">Description</Typography.Text>
<TextArea
placeholder="Enter the panel description here..."
bordered
allowClear
value={description}
onChange={(event): void =>
onChangeHandler(setDescription, event.target.value)
}
// Only update the y-axis unit value automatically in create mode
shouldUpdateYAxisUnit={isNewDashboard}
rootClassName="description-input"
/>
)}
</section>
</SettingsSection>
{allowDecimalPrecision && (
<section className="decimal-precision-selector">
<Typography.Text className="typography">
Decimal Precision
</Typography.Text>
<section className="panel-config">
<SettingsSection
title="Visualization"
defaultOpen
icon={<LayoutDashboard size={14} />}
>
<section className="panel-type control-container">
<Typography.Text className="typography">Panel Type</Typography.Text>
<Select
options={[
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
{ label: '4 decimals', value: PrecisionOptionsEnum.FOUR },
{ label: 'Full Precision', value: PrecisionOptionsEnum.FULL },
]}
value={decimalPrecision}
style={{ width: '100%' }}
onChange={setGraphHandler}
value={selectedGraph}
className="panel-type-select"
defaultValue={PrecisionOptionsEnum.TWO}
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
/>
data-testid="panel-change-select"
data-stacking-state={stackedBarChart ? 'true' : 'false'}
>
{graphTypes.map((item) => (
<Option key={item.name} value={item.name}>
<div className="select-option">
<div className="icon">{item.icon}</div>
<Typography.Text className="display">{item.display}</Typography.Text>
</div>
</Option>
))}
</Select>
</section>
)}
{allowSoftMinMax && (
<section className="soft-min-max">
<section className="container">
<Typography.Text className="text">Soft Min</Typography.Text>
<InputNumber
type="number"
value={softMin}
onChange={softMinHandler}
rootClassName="input"
{allowPanelTimePreference && (
<section className="panel-time-preference control-container">
<Typography.Text className="panel-time-text">
Panel Time Preference
</Typography.Text>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
/>
</section>
<section className="container">
<Typography.Text className="text">Soft Max</Typography.Text>
<InputNumber
value={softMax}
type="number"
rootClassName="input"
onChange={softMaxHandler}
)}
{allowStackingBarChart && (
<section className="stack-chart control-container">
<Typography.Text className="label">Stack series</Typography.Text>
<Switch
checked={stackedBarChart}
size="small"
onChange={(checked): void => setStackedBarChart(checked)}
/>
</section>
</section>
)}
{allowFillSpans && (
<section className="fill-gaps">
<div className="fill-gaps-text-container">
<Typography className="fill-gaps-text">Fill gaps</Typography>
<Typography.Text className="fill-gaps-text-description">
Fill gaps in data with 0 for continuity
</Typography.Text>
</div>
<Switch
checked={isFillSpans}
size="small"
onChange={(checked): void => setIsFillSpans(checked)}
/>
</section>
)}
</SettingsSection>
{isFormattingSectionVisible && (
<SettingsSection
title="Formatting & Units"
icon={<SlidersHorizontal size={14} />}
>
{allowYAxisUnit && (
<DashboardYAxisUnitSelectorWrapper
onSelect={setYAxisUnit}
value={yAxisUnit || ''}
fieldLabel={
selectedGraphType === PanelDisplay.VALUE ||
selectedGraphType === PanelDisplay.PIE
? 'Unit'
: 'Y Axis Unit'
}
// Only update the y-axis unit value automatically in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/>
)}
{allowDecimalPrecision && (
<section className="decimal-precision-selector control-container">
<Typography.Text className="typography">
Decimal Precision
</Typography.Text>
<Select
options={decimapPrecisionOptions}
value={decimalPrecision}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={PrecisionOptionsEnum.TWO}
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
/>
</section>
)}
{allowPanelColumnPreference && (
<ColumnUnitSelector
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
isNewDashboard={isNewDashboard}
/>
)}
</SettingsSection>
)}
{allowStackingBarChart && (
<section className="stack-chart">
<Typography.Text className="label">Stack series</Typography.Text>
<Switch
checked={stackedBarChart}
size="small"
onChange={(checked): void => setStackedBarChart(checked)}
/>
</section>
{isAxisSectionVisible && (
<SettingsSection title="Axes" icon={<Axis3D size={14} />}>
{allowSoftMinMax && (
<section className="soft-min-max">
<section className="container">
<Typography.Text className="text">Soft Min</Typography.Text>
<InputNumber
type="number"
value={softMin}
onChange={softMinHandler}
rootClassName="input"
/>
</section>
<section className="container">
<Typography.Text className="text">Soft Max</Typography.Text>
<InputNumber
value={softMax}
type="number"
rootClassName="input"
onChange={softMaxHandler}
/>
</section>
</section>
)}
{allowLogScale && (
<section className="log-scale control-container">
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
<Select
onChange={(value): void =>
setIsLogScale(value === LogScale.LOGARITHMIC)
}
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LogScale.LINEAR}
>
<Option value={LogScale.LINEAR}>
<div className="select-option">
<div className="icon">
<LineChart size={16} />
</div>
<Typography.Text className="display">Linear</Typography.Text>
</div>
</Option>
<Option value={LogScale.LOGARITHMIC}>
<div className="select-option">
<div className="icon">
<Spline size={16} />
</div>
<Typography.Text className="display">Logarithmic</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
</SettingsSection>
)}
{isLegendSectionVisible && (
<SettingsSection title="Legend" icon={<Layers size={14} />}>
{allowLegendPosition && (
<section className="legend-position control-container">
<Typography.Text className="typography">Position</Typography.Text>
<Select
onChange={(value: LegendPosition): void => setLegendPosition(value)}
value={legendPosition}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LegendPosition.BOTTOM}
>
<Option value={LegendPosition.BOTTOM}>
<div className="select-option">
<Typography.Text className="display">Bottom</Typography.Text>
</div>
</Option>
<Option value={LegendPosition.RIGHT}>
<div className="select-option">
<Typography.Text className="display">Right</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
{allowLegendColors && (
<section className="legend-colors">
<LegendColors
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
/>
</section>
)}
</SettingsSection>
)}
{allowBucketConfig && (
<section className="bucket-config">
<Typography.Text className="label">Number of buckets</Typography.Text>
<InputNumber
value={bucketCount || null}
type="number"
min={0}
rootClassName="bucket-input"
placeholder="Default: 30"
onChange={(val): void => {
setBucketCount(val || 0);
}}
/>
<Typography.Text className="label bucket-size-label">
Bucket width
</Typography.Text>
<InputNumber
value={bucketWidth || null}
type="number"
precision={2}
placeholder="Default: Auto"
step={0.1}
min={0.0}
rootClassName="bucket-input"
onChange={(val): void => {
setBucketWidth(val || 0);
}}
/>
<section className="combine-hist">
<Typography.Text className="label">
Merge all series into one
</Typography.Text>
<Switch
checked={combineHistogram}
size="small"
onChange={(checked): void => setCombineHistogram(checked)}
<SettingsSection title="Histogram / Buckets">
<section className="bucket-config control-container">
<Typography.Text className="label">Number of buckets</Typography.Text>
<InputNumber
value={bucketCount || null}
type="number"
min={0}
rootClassName="bucket-input"
placeholder="Default: 30"
onChange={(val): void => {
setBucketCount(val || 0);
}}
/>
<Typography.Text className="label bucket-size-label">
Bucket width
</Typography.Text>
<InputNumber
value={bucketWidth || null}
type="number"
precision={2}
placeholder="Default: Auto"
step={0.1}
min={0.0}
rootClassName="bucket-input"
onChange={(val): void => {
setBucketWidth(val || 0);
}}
/>
<section className="combine-hist">
<Typography.Text className="label">
Merge all series into one
</Typography.Text>
<Switch
checked={combineHistogram}
size="small"
onChange={(checked): void => setCombineHistogram(checked)}
/>
</section>
</section>
</section>
)}
{allowLogScale && (
<section className="log-scale">
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
<Select
onChange={(value): void => setIsLogScale(value === LogScale.LOGARITHMIC)}
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LogScale.LINEAR}
>
<Option value={LogScale.LINEAR}>
<div className="select-option">
<div className="icon">
<LineChart size={16} />
</div>
<Typography.Text className="display">Linear</Typography.Text>
</div>
</Option>
<Option value={LogScale.LOGARITHMIC}>
<div className="select-option">
<div className="icon">
<Spline size={16} />
</div>
<Typography.Text className="display">Logarithmic</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
{allowLegendPosition && (
<section className="legend-position">
<Typography.Text className="typography">Legend Position</Typography.Text>
<Select
onChange={(value: LegendPosition): void => setLegendPosition(value)}
value={legendPosition}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LegendPosition.BOTTOM}
>
<Option value={LegendPosition.BOTTOM}>
<div className="select-option">
<Typography.Text className="display">Bottom</Typography.Text>
</div>
</Option>
<Option value={LegendPosition.RIGHT}>
<div className="select-option">
<Typography.Text className="display">Right</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
{allowLegendColors && (
<section className="legend-colors">
<LegendColors
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
/>
</section>
</SettingsSection>
)}
</section>
@@ -541,17 +600,25 @@ function RightContainer({
)}
{allowContextLinks && (
<section className="context-links">
<SettingsSection
title="Context Links"
icon={<Link size={14} />}
defaultOpen={!!contextLinks.linksData.length}
>
<ContextLinks
contextLinks={contextLinks}
setContextLinks={setContextLinks}
selectedWidget={selectedWidget}
/>
</section>
</SettingsSection>
)}
{allowThreshold && (
<section>
<SettingsSection
title="Thresholds"
icon={<Antenna size={14} />}
defaultOpen={!!thresholds.length}
>
<ThresholdSelector
thresholds={thresholds}
setThresholds={setThresholds}
@@ -559,7 +626,7 @@ function RightContainer({
selectedGraph={selectedGraph}
columnUnits={columnUnits}
/>
</section>
</SettingsSection>
)}
</div>
);

View File

@@ -36,7 +36,7 @@ const checkStackSeriesState = (
expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument();
const stackSeriesSection = container.querySelector(
'section > .stack-chart',
'.stack-chart',
) as HTMLElement;
expect(stackSeriesSection).toBeInTheDocument();
@@ -310,12 +310,12 @@ describe('Stacking bar in new panel', () => {
const { container, getByText } = render(
<I18nextProvider i18n={i18n}>
<DashboardProvider>
<DashboardProvider dashboardId="">
<PreferenceContextProvider>
<NewWidget
dashboardId=""
selectedDashboard={undefined}
selectedGraph={PANEL_TYPES.BAR}
fillSpans={undefined}
yAxisUnit={undefined}
/>
</PreferenceContextProvider>
</DashboardProvider>
@@ -326,7 +326,7 @@ describe('Stacking bar in new panel', () => {
expect(getByText('Stack series')).toBeInTheDocument();
// Verify section exists
const section = container.querySelector('section > .stack-chart');
const section = container.querySelector('.stack-chart');
expect(section).toBeInTheDocument();
// Verify switch is present and enabled (ant-switch-checked)
@@ -356,11 +356,11 @@ describe('when switching to BAR panel type', () => {
it('should preserve saved stacking value of true', async () => {
const { getByTestId, getByText, container } = render(
<DashboardProvider>
<DashboardProvider dashboardId="">
<NewWidget
dashboardId=""
selectedDashboard={undefined}
selectedGraph={PANEL_TYPES.BAR}
fillSpans={undefined}
yAxisUnit={undefined}
/>
</DashboardProvider>,
);

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { generatePath, useParams } from 'react-router-dom';
import { generatePath } from 'react-router-dom';
import { WarningOutlined } from '@ant-design/icons';
import { Button, Flex, Modal, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -32,8 +32,7 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
import { cloneDeep, defaultTo, isEmpty, isUndefined } from 'lodash-es';
import { Check, X } from 'lucide-react';
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useScrollToWidgetIdStore } from 'providers/Dashboard/helpers/scrollToWidgetIdHelper';
import {
clearSelectedRowWidgetId,
getSelectedRowWidgetId,
@@ -82,16 +81,15 @@ import {
import './NewWidget.styles.scss';
function NewWidget({
selectedDashboard,
dashboardId,
selectedGraph,
enableDrillDown = false,
}: NewWidgetProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const {
selectedDashboard,
setSelectedDashboard,
setToScrollWidgetId,
columnWidths,
} = useDashboard();
const setToScrollWidgetId = useScrollToWidgetIdStore(
(s) => s.setToScrollWidgetId,
);
const { dashboardVariables } = useDashboardVariables();
@@ -136,8 +134,6 @@ function NewWidget({
const query = useUrlQuery();
const { dashboardId } = useParams<DashboardWidgetPageParams>();
const [isNewDashboard, setIsNewDashboard] = useState<boolean>(false);
const logEventCalledRef = useRef(false);
@@ -283,11 +279,10 @@ function NewWidget({
isLogScale,
legendPosition,
customLegendColors,
columnWidths: columnWidths?.[selectedWidget?.id],
columnWidths: selectedWidget.columnWidths,
contextLinks,
};
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
columnUnits,
currentQuery,
@@ -310,8 +305,8 @@ function NewWidget({
isLogScale,
legendPosition,
customLegendColors,
columnWidths,
contextLinks,
selectedWidget.columnWidths,
]);
const closeModal = (): void => {
@@ -444,6 +439,19 @@ function NewWidget({
globalSelectedInterval,
]);
const navigateToDashboardPage = useCallback(() => {
const params = new URLSearchParams();
const urlVariablesQueryString = query.get(QueryParams.variables);
if (urlVariablesQueryString) {
params.set(QueryParams.variables, urlVariablesQueryString);
}
const search = params.toString() ? `?${params.toString()}` : '';
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }) + search);
}, [dashboardId, query, safeNavigate]);
const onClickSaveHandler = useCallback(() => {
if (!selectedDashboard) {
return;
@@ -557,12 +565,9 @@ function NewWidget({
};
updateDashboardMutation.mutateAsync(dashboard, {
onSuccess: (updatedDashboard) => {
setSelectedDashboard(updatedDashboard.data);
onSuccess: () => {
setToScrollWidgetId(selectedWidget?.id || '');
safeNavigate({
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
});
navigateToDashboardPage();
},
});
}, [
@@ -577,9 +582,8 @@ function NewWidget({
preWidgets,
updateDashboardMutation,
widgets,
setSelectedDashboard,
setToScrollWidgetId,
safeNavigate,
navigateToDashboardPage,
dashboardId,
]);
@@ -588,12 +592,12 @@ function NewWidget({
setDiscardModal(true);
return;
}
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId, isQueryModified, safeNavigate]);
navigateToDashboardPage();
}, [isQueryModified, navigateToDashboardPage]);
const discardChanges = useCallback(() => {
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId, safeNavigate]);
navigateToDashboardPage();
}, [navigateToDashboardPage]);
const setGraphHandler = (type: PANEL_TYPES): void => {
setIsLoadingPanelData(true);
@@ -627,22 +631,25 @@ function NewWidget({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query]);
const onSaveDashboard = useCallback((): void => {
const isNewPanel = useMemo(() => {
const widgetId = query.get('widgetId');
const selectWidget = widgets?.find((e) => e.id === widgetId);
const selectedWidget = widgets?.find((e) => e.id === widgetId);
return isUndefined(selectedWidget);
}, [query, widgets]);
const onSaveDashboard = useCallback((): void => {
logEvent('Panel Edit: Save changes', {
panelType: selectedWidget.panelTypes,
dashboardId: selectedDashboard?.id,
widgetId: selectedWidget.id,
dashboardName: selectedDashboard?.data.title,
queryType: currentQuery.queryType,
isNewPanel: isUndefined(selectWidget),
isNewPanel,
dataSource: currentQuery?.builder?.queryData?.[0]?.dataSource,
});
setSaveModal(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [isNewPanel]);
const isNewTraceLogsAvailable =
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
@@ -732,12 +739,14 @@ function NewWidget({
}
const widgetId = query.get('widgetId') || '';
const graphType = query.get('graphType') || '';
const variables = query.get(QueryParams.variables) || '';
const queryParams = {
[QueryParams.expandedWidgetId]: widgetId,
[QueryParams.graphType]: graphType,
[QueryParams.compositeQuery]: encodeURIComponent(
JSON.stringify(currentQuery),
),
[QueryParams.variables]: variables,
};
const updatedSearch = createQueryParams(queryParams);
@@ -818,6 +827,8 @@ function NewWidget({
isLoadingPanelData={isLoadingPanelData}
setQueryResponse={setQueryResponse}
enableDrillDown={enableDrillDown}
selectedDashboard={selectedDashboard}
isNewPanel={isNewPanel}
/>
)}
</OverlayScrollbar>

View File

@@ -2,6 +2,7 @@ import { Dispatch, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { IDashboardContext } from 'providers/Dashboard/types';
import { SuccessResponse, Warning } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
@@ -9,9 +10,9 @@ import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { timePreferance } from './RightContainer/timeItems';
export interface NewWidgetProps {
dashboardId: string;
selectedDashboard: IDashboardContext['selectedDashboard'];
selectedGraph: PANEL_TYPES;
yAxisUnit: Widgets['yAxisUnit'];
fillSpans: Widgets['fillSpans'];
enableDrillDown?: boolean;
}
@@ -34,6 +35,8 @@ export interface WidgetGraphProps {
>
>;
enableDrillDown?: boolean;
selectedDashboard: IDashboardContext['selectedDashboard'];
isNewPanel?: boolean;
}
export type WidgetGraphContainerProps = {

View File

@@ -1,180 +0,0 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { ToggleGraphProps } from 'components/Graph/types';
import Uplot from 'components/Uplot';
import GraphManager from 'container/GridCardLayout/GridCard/FullView/GraphManager';
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
import { getUplotClickData } from 'container/QueryTable/Drilldown/drilldownUtils';
import useGraphContextMenu from 'container/QueryTable/Drilldown/useGraphContextMenu';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { getUplotHistogramChartOptions } from 'lib/uPlotLib/getUplotHistogramChartOptions';
import _noop from 'lodash-es/noop';
import { ContextMenu, useCoordinates } from 'periscope/components/ContextMenu';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { buildHistogramData } from './histogram';
import { PanelWrapperProps } from './panelWrapper.types';
function HistogramPanelWrapper({
queryResponse,
widget,
setGraphVisibility,
graphVisibility,
isFullViewMode,
onToggleModelHandler,
onClickHandler,
enableDrillDown = false,
}: PanelWrapperProps): JSX.Element {
const graphRef = useRef<HTMLDivElement>(null);
const legendScrollPositionRef = useRef<number>(0);
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const isDarkMode = useIsDarkMode();
const containerDimensions = useResizeObserver(graphRef);
const {
coordinates,
popoverPosition,
clickedData,
onClose,
onClick,
subMenu,
setSubMenu,
} = useCoordinates();
const { menuItemsConfig } = useGraphContextMenu({
widgetId: widget.id || '',
query: widget.query,
graphData: clickedData,
onClose,
coordinates,
subMenu,
setSubMenu,
contextLinks: widget.contextLinks,
panelType: widget.panelTypes,
queryRange: queryResponse,
});
const clickHandlerWithContextMenu = useCallback(
(...args: any[]) => {
const [
,
,
,
,
metric,
queryData,
absoluteMouseX,
absoluteMouseY,
,
focusedSeries,
] = args;
const data = getUplotClickData({
metric,
queryData,
absoluteMouseX,
absoluteMouseY,
focusedSeries,
});
if (data && data?.record?.queryName) {
onClick(data.coord, { ...data.record, label: data.label });
}
},
[onClick],
);
const histogramData = buildHistogramData(
queryResponse.data?.payload.data.result,
widget?.bucketWidth,
widget?.bucketCount,
widget?.mergeAllActiveQueries,
);
useEffect(() => {
if (toScrollWidgetId === widget.id) {
graphRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
graphRef.current?.focus();
setToScrollWidgetId('');
}
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
const lineChartRef = useRef<ToggleGraphProps>();
useEffect(() => {
const {
graphVisibilityStates: localStoredVisibilityState,
} = getLocalStorageGraphVisibilityState({
apiResponse: queryResponse.data?.payload.data.result || [],
name: widget.id,
});
if (setGraphVisibility) {
setGraphVisibility(localStoredVisibilityState);
}
}, [
queryResponse?.data?.payload?.data?.result,
setGraphVisibility,
widget.id,
]);
const histogramOptions = useMemo(
() =>
getUplotHistogramChartOptions({
id: widget.id,
dimensions: containerDimensions,
isDarkMode,
apiResponse: queryResponse.data?.payload,
histogramData,
panelType: widget.panelTypes,
setGraphsVisibilityStates: setGraphVisibility,
graphsVisibilityStates: graphVisibility,
mergeAllQueries: widget.mergeAllActiveQueries,
onClickHandler: enableDrillDown
? clickHandlerWithContextMenu
: onClickHandler ?? _noop,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: number) => {
legendScrollPositionRef.current = position;
},
}),
[
containerDimensions,
graphVisibility,
histogramData,
isDarkMode,
queryResponse.data?.payload,
setGraphVisibility,
widget.id,
widget.mergeAllActiveQueries,
widget.panelTypes,
clickHandlerWithContextMenu,
enableDrillDown,
onClickHandler,
],
);
return (
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
<Uplot options={histogramOptions} data={histogramData} ref={lineChartRef} />
<ContextMenu
coordinates={coordinates}
popoverPosition={popoverPosition}
title={menuItemsConfig.header as string}
items={menuItemsConfig.items}
onClose={onClose}
/>
{isFullViewMode && setGraphVisibility && !widget.mergeAllActiveQueries && (
<GraphManager
data={histogramData}
name={widget.id}
options={histogramOptions}
yAxisUnit={widget.yAxisUnit}
onToggleModelHandler={onToggleModelHandler}
setGraphsVisibilityStates={setGraphVisibility}
graphsVisibilityStates={graphVisibility}
lineChartRef={lineChartRef}
/>
)}
</div>
);
}
export default HistogramPanelWrapper;

View File

@@ -1,4 +0,0 @@
.info-text {
margin-top: 8px;
padding: 8px;
}

View File

@@ -1,318 +0,0 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Alert } from 'antd';
import { ToggleGraphProps } from 'components/Graph/types';
import Uplot from 'components/Uplot';
import { PANEL_TYPES } from 'constants/queryBuilder';
import GraphManager from 'container/GridCardLayout/GridCard/FullView/GraphManager';
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
import { getUplotClickData } from 'container/QueryTable/Drilldown/drilldownUtils';
import useGraphContextMenu from 'container/QueryTable/Drilldown/useGraphContextMenu';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { cloneDeep, isEqual, isUndefined } from 'lodash-es';
import _noop from 'lodash-es/noop';
import { ContextMenu, useCoordinates } from 'periscope/components/ContextMenu';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useTimezone } from 'providers/Timezone';
import { DataSource } from 'types/common/queryBuilder';
import uPlot from 'uplot';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
import { PanelWrapperProps } from './panelWrapper.types';
import { getTimeRangeFromStepInterval, isApmMetric } from './utils';
import './UplotPanelWrapper.styles.scss';
function UplotPanelWrapper({
queryResponse,
widget,
isFullViewMode,
setGraphVisibility,
graphVisibility,
onToggleModelHandler,
onClickHandler,
onDragSelect,
selectedGraph,
customTooltipElement,
customSeries,
enableDrillDown = false,
}: PanelWrapperProps): JSX.Element {
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const isDarkMode = useIsDarkMode();
const lineChartRef = useRef<ToggleGraphProps>();
const graphRef = useRef<HTMLDivElement>(null);
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const { currentQuery } = useQueryBuilder();
const [hiddenGraph, setHiddenGraph] = useState<{ [key: string]: boolean }>();
useEffect(() => {
if (toScrollWidgetId === widget.id) {
graphRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
graphRef.current?.focus();
setToScrollWidgetId('');
}
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(queryResponse);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [queryResponse]);
const containerDimensions = useResizeObserver(graphRef);
const {
coordinates,
popoverPosition,
clickedData,
onClose,
onClick,
subMenu,
setSubMenu,
} = useCoordinates();
const { menuItemsConfig } = useGraphContextMenu({
widgetId: widget.id || '',
query: widget.query,
graphData: clickedData,
onClose,
coordinates,
subMenu,
setSubMenu,
contextLinks: widget.contextLinks,
panelType: widget.panelTypes,
queryRange: queryResponse,
});
useEffect(() => {
const {
graphVisibilityStates: localStoredVisibilityState,
} = getLocalStorageGraphVisibilityState({
apiResponse: queryResponse.data?.payload.data.result || [],
name: widget.id,
});
if (setGraphVisibility) {
setGraphVisibility(localStoredVisibilityState);
}
}, [
queryResponse?.data?.payload?.data?.result,
setGraphVisibility,
widget.id,
]);
if (queryResponse.data && widget.panelTypes === PANEL_TYPES.BAR) {
const sortedSeriesData = getSortedSeriesData(
queryResponse.data?.payload.data.result,
);
queryResponse.data.payload.data.result = sortedSeriesData;
}
const stackedBarChart = useMemo(
() =>
(selectedGraph
? selectedGraph === PANEL_TYPES.BAR
: widget?.panelTypes === PANEL_TYPES.BAR) && widget?.stackedBarChart,
[selectedGraph, widget?.panelTypes, widget?.stackedBarChart],
);
const chartData = useMemo(
() =>
getUPlotChartData(
queryResponse?.data?.payload,
widget.fillSpans,
stackedBarChart,
hiddenGraph,
),
[
queryResponse?.data?.payload,
widget.fillSpans,
stackedBarChart,
hiddenGraph,
],
);
useEffect(() => {
if (widget.panelTypes === PANEL_TYPES.BAR && stackedBarChart) {
const graphV = cloneDeep(graphVisibility)?.slice(1);
const isSomeSelectedLegend = graphV?.some((v) => v === false);
if (isSomeSelectedLegend) {
const hiddenIndex = graphV?.findIndex((v) => v === true);
if (!isUndefined(hiddenIndex) && hiddenIndex !== -1) {
const updatedHiddenGraph = { [hiddenIndex]: true };
if (!isEqual(hiddenGraph, updatedHiddenGraph)) {
setHiddenGraph(updatedHiddenGraph);
}
}
}
}
}, [graphVisibility, hiddenGraph, widget.panelTypes, stackedBarChart]);
const { timezone } = useTimezone();
const clickHandlerWithContextMenu = useCallback(
(...args: any[]) => {
const [
xValue,
,
,
,
metric,
queryData,
absoluteMouseX,
absoluteMouseY,
axesData,
focusedSeries,
] = args;
const data = getUplotClickData({
metric,
queryData,
absoluteMouseX,
absoluteMouseY,
focusedSeries,
});
// Compute time range if needed and if axes data is available
let timeRange;
if (axesData && queryData?.queryName) {
// Get the compositeQuery from the response params
const compositeQuery = (queryResponse?.data?.params as any)?.compositeQuery;
if (compositeQuery?.queries) {
// Find the specific query by name from the queries array
const specificQuery = compositeQuery.queries.find(
(query: any) => query.spec?.name === queryData.queryName,
);
// Use the stepInterval from the specific query, fallback to default
const stepInterval = specificQuery?.spec?.stepInterval || 60;
timeRange = getTimeRangeFromStepInterval(
stepInterval,
metric?.clickedTimestamp || xValue, // Use the clicked timestamp if available, otherwise use the click position timestamp
specificQuery?.spec?.signal === DataSource.METRICS &&
isApmMetric(specificQuery?.spec?.aggregations[0]?.metricName),
);
}
}
if (data && data?.record?.queryName) {
onClick(data.coord, { ...data.record, label: data.label, timeRange });
}
},
[onClick, queryResponse],
);
const options = useMemo(
() =>
getUPlotChartOptions({
id: widget?.id,
apiResponse: queryResponse.data?.payload,
dimensions: containerDimensions,
isDarkMode,
onDragSelect,
yAxisUnit: widget?.yAxisUnit,
onClickHandler: enableDrillDown
? clickHandlerWithContextMenu
: onClickHandler ?? _noop,
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
softMax: widget.softMax === undefined ? null : widget.softMax,
softMin: widget.softMin === undefined ? null : widget.softMin,
graphsVisibilityStates: graphVisibility,
setGraphsVisibilityStates: setGraphVisibility,
panelType: selectedGraph || widget.panelTypes,
currentQuery,
stackBarChart: stackedBarChart,
hiddenGraph,
setHiddenGraph,
customTooltipElement,
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
customSeries,
isLogScale: widget?.isLogScale,
colorMapping: widget?.customLegendColors,
enhancedLegend: true, // Enable enhanced legend
legendPosition: widget?.legendPosition,
query: widget?.query || currentQuery,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
decimalPrecision: widget.decimalPrecision,
}),
[
queryResponse.data?.payload,
containerDimensions,
isDarkMode,
onDragSelect,
clickHandlerWithContextMenu,
minTimeScale,
maxTimeScale,
graphVisibility,
setGraphVisibility,
selectedGraph,
currentQuery,
hiddenGraph,
customTooltipElement,
timezone.value,
customSeries,
enableDrillDown,
onClickHandler,
widget,
stackedBarChart,
],
);
return (
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
<Uplot options={options} data={chartData} ref={lineChartRef} />
<ContextMenu
coordinates={coordinates}
popoverPosition={popoverPosition}
title={menuItemsConfig.header as string}
items={menuItemsConfig.items}
onClose={onClose}
/>
{stackedBarChart && isFullViewMode && (
<Alert
message="Selecting multiple legends is currently not supported in case of stacked bar charts"
type="info"
className="info-text"
/>
)}
{isFullViewMode && setGraphVisibility && !stackedBarChart && (
<GraphManager
data={chartData}
name={widget.id}
options={options}
yAxisUnit={widget.yAxisUnit}
onToggleModelHandler={onToggleModelHandler}
setGraphsVisibilityStates={setGraphVisibility}
graphsVisibilityStates={graphVisibility}
lineChartRef={lineChartRef}
/>
)}
</div>
);
}
export default UplotPanelWrapper;

View File

@@ -38,6 +38,7 @@ export const routeConfig: Record<string, QueryParams[]> = {
[ROUTES.MY_SETTINGS]: [QueryParams.resourceAttributes],
[ROUTES.NOT_FOUND]: [QueryParams.resourceAttributes],
[ROUTES.ORG_SETTINGS]: [QueryParams.resourceAttributes],
[ROUTES.MEMBERS_SETTINGS]: [QueryParams.resourceAttributes],
[ROUTES.PASSWORD_RESET]: [QueryParams.resourceAttributes],
[ROUTES.SETTINGS]: [QueryParams.resourceAttributes],
[ROUTES.SIGN_UP]: [QueryParams.resourceAttributes],

View File

@@ -0,0 +1,339 @@
import { renderHook } from '@testing-library/react';
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
jest.mock('hooks/dashboard/useDashboardFromLocalStorage');
jest.mock('hooks/dashboard/useVariablesFromUrl');
const mockUseDashboardVariablesFromLocalStorage = useDashboardVariablesFromLocalStorage as jest.MockedFunction<
typeof useDashboardVariablesFromLocalStorage
>;
const mockUseVariablesFromUrl = useVariablesFromUrl as jest.MockedFunction<
typeof useVariablesFromUrl
>;
const makeVariable = (
overrides: Partial<IDashboardVariable> = {},
): IDashboardVariable => ({
id: 'existing-id',
name: 'env',
description: '',
type: 'QUERY',
sort: 'DISABLED',
multiSelect: false,
showALLOption: false,
selectedValue: 'prod',
...overrides,
});
const makeDashboard = (
variables: Record<string, IDashboardVariable>,
): Dashboard => ({
id: 'dash-1',
createdAt: '',
updatedAt: '',
createdBy: '',
updatedBy: '',
data: {
title: 'Test',
variables,
},
});
const setupHook = (
currentDashboard: Record<string, any> = {},
urlVariables: Record<string, any> = {},
): ReturnType<typeof useTransformDashboardVariables> => {
mockUseDashboardVariablesFromLocalStorage.mockReturnValue({
currentDashboard,
updateLocalStorageDashboardVariables: jest.fn(),
});
mockUseVariablesFromUrl.mockReturnValue({
getUrlVariables: () => urlVariables,
setUrlVariables: jest.fn(),
updateUrlVariable: jest.fn(),
});
const { result } = renderHook(() => useTransformDashboardVariables('dash-1'));
return result.current;
};
describe('useTransformDashboardVariables', () => {
beforeEach(() => jest.clearAllMocks());
describe('order assignment', () => {
it('assigns order starting from 0 to variables that have none', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({ id: 'id1', name: 'v1', order: undefined }),
v2: makeVariable({ id: 'id2', name: 'v2', order: undefined }),
});
const result = transformDashboardVariables(dashboard);
const orders = Object.values(result.data.variables).map((v) => v.order);
expect(orders).toContain(0);
expect(orders).toContain(1);
});
it('preserves existing order values', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({ id: 'id1', name: 'v1', order: 5 }),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.order).toBe(5);
});
it('assigns unique orders across multiple variables that all lack an order', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({ id: 'id1', name: 'v1', order: undefined }),
v2: makeVariable({ id: 'id2', name: 'v2', order: undefined }),
v3: makeVariable({ id: 'id3', name: 'v3', order: undefined }),
});
const result = transformDashboardVariables(dashboard);
const orders = Object.values(result.data.variables).map((v) => v.order);
// All three newly assigned orders must be distinct
expect(new Set(orders).size).toBe(3);
});
});
describe('ID assignment', () => {
it('assigns a UUID to variables that have no id', () => {
const { transformDashboardVariables } = setupHook();
const variable = makeVariable({ name: 'v1' });
(variable as any).id = undefined;
const dashboard = makeDashboard({ v1: variable });
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.id).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
);
});
it('preserves existing IDs', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({ id: 'keep-me', name: 'v1' }),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.id).toBe('keep-me');
});
});
describe('TEXTBOX backward compatibility', () => {
it('copies textboxValue to defaultValue when defaultValue is missing', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'v1',
type: 'TEXTBOX',
textboxValue: 'hello',
defaultValue: undefined,
order: undefined,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.defaultValue).toBe('hello');
});
it('does not overwrite an existing defaultValue', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'v1',
type: 'TEXTBOX',
textboxValue: 'old',
defaultValue: 'keep',
order: undefined,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.defaultValue).toBe('keep');
});
});
describe('localStorage merge', () => {
it('applies localStorage selectedValue over DB value', () => {
const { transformDashboardVariables } = setupHook({
env: { selectedValue: 'staging', allSelected: false },
});
const dashboard = makeDashboard({
v1: makeVariable({ id: 'id1', name: 'env', selectedValue: 'prod' }),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toBe('staging');
});
it('applies localStorage allSelected over DB value', () => {
const { transformDashboardVariables } = setupHook({
env: { selectedValue: undefined, allSelected: true },
});
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
allSelected: false,
showALLOption: true,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.allSelected).toBe(true);
});
});
describe('URL variable override', () => {
it('sets allSelected=true when URL value is __ALL__', () => {
const { transformDashboardVariables } = setupHook(
{ env: { selectedValue: 'prod', allSelected: false } },
{ env: '__ALL__' },
);
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
showALLOption: true,
allSelected: false,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.allSelected).toBe(true);
});
it('sets selectedValue from URL and clears allSelected when showALLOption is true', () => {
const { transformDashboardVariables } = setupHook(
{ env: { selectedValue: undefined, allSelected: true } },
{ env: 'dev' },
);
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
showALLOption: true,
allSelected: true,
multiSelect: false,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toBe('dev');
expect(result.data.variables.v1.allSelected).toBe(false);
});
it('does not set allSelected=false when showALLOption is false', () => {
const { transformDashboardVariables } = setupHook(
{ env: { selectedValue: undefined, allSelected: true } },
{ env: 'dev' },
);
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
showALLOption: false,
allSelected: true,
multiSelect: false,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toBe('dev');
expect(result.data.variables.v1.allSelected).toBe(true);
});
it('normalizes array URL value to single value for single-select variable', () => {
const { transformDashboardVariables } = setupHook(
{},
{ env: ['prod', 'dev'] },
);
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
multiSelect: false,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toBe('prod');
});
it('wraps single URL value in array for multi-select variable', () => {
const { transformDashboardVariables } = setupHook({}, { env: 'prod' });
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
multiSelect: true,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toEqual(['prod']);
});
it('looks up URL variable by variable id when name is absent', () => {
const { transformDashboardVariables } = setupHook(
{},
{ 'var-uuid': 'fallback' },
);
const variable = makeVariable({ id: 'var-uuid', multiSelect: false });
delete variable.name;
const dashboard = makeDashboard({ v1: variable });
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toBe('fallback');
});
});
describe('edge cases', () => {
it('returns data unchanged when there are no variables', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables).toEqual({});
});
it('does not mutate the original dashboard', () => {
const { transformDashboardVariables } = setupHook({
env: { selectedValue: 'staging', allSelected: false },
});
const dashboard = makeDashboard({
v1: makeVariable({ id: 'id1', name: 'env', selectedValue: 'prod' }),
});
const originalValue = dashboard.data.variables.v1.selectedValue;
transformDashboardVariables(dashboard);
expect(dashboard.data.variables.v1.selectedValue).toBe(originalValue);
});
});
});

View File

@@ -15,7 +15,7 @@ interface DashboardLocalStorageVariables {
[id: string]: LocalStoreDashboardVariables;
}
interface UseDashboardVariablesFromLocalStorageReturn {
export interface UseDashboardVariablesFromLocalStorageReturn {
currentDashboard: LocalStoreDashboardVariables;
updateLocalStorageDashboardVariables: (
id: string,

View File

@@ -0,0 +1,128 @@
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
import {
useDashboardVariablesFromLocalStorage,
UseDashboardVariablesFromLocalStorageReturn,
} from 'hooks/dashboard/useDashboardFromLocalStorage';
import useVariablesFromUrl, {
UseVariablesFromUrlReturn,
} from 'hooks/dashboard/useVariablesFromUrl';
import { isEmpty } from 'lodash-es';
import { normalizeUrlValueForVariable } from 'providers/Dashboard/normalizeUrlValue';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import { v4 as generateUUID } from 'uuid';
export function useTransformDashboardVariables(
dashboardId: string,
): Pick<UseVariablesFromUrlReturn, 'getUrlVariables' | 'updateUrlVariable'> &
UseDashboardVariablesFromLocalStorageReturn & {
transformDashboardVariables: (data: Dashboard) => Dashboard;
} {
const {
currentDashboard,
updateLocalStorageDashboardVariables,
} = useDashboardVariablesFromLocalStorage(dashboardId);
const { getUrlVariables, updateUrlVariable } = useVariablesFromUrl();
const mergeDBWithLocalStorage = (
data: Dashboard,
localStorageVariables: any,
): Dashboard => {
const updatedData = data;
if (data && localStorageVariables) {
const updatedVariables = data.data.variables;
const variablesFromUrl = getUrlVariables();
Object.keys(data.data.variables).forEach((variable) => {
const variableData = data.data.variables[variable];
// values from url
const urlVariable = variableData?.name
? variablesFromUrl[variableData?.name] || variablesFromUrl[variableData.id]
: variablesFromUrl[variableData.id];
let updatedVariable = {
...data.data.variables[variable],
...localStorageVariables[variableData.name as any],
};
// respect the url variable if it is set, override the others
if (!isEmpty(urlVariable)) {
if (urlVariable === ALL_SELECTED_VALUE) {
updatedVariable = {
...updatedVariable,
allSelected: true,
};
} else {
// Normalize URL value to match variable's multiSelect configuration
const normalizedValue = normalizeUrlValueForVariable(
urlVariable,
variableData,
);
updatedVariable = {
...updatedVariable,
selectedValue: normalizedValue,
// Only set allSelected to false if showALLOption is available
...(updatedVariable?.showALLOption && { allSelected: false }),
};
}
}
updatedVariables[variable] = updatedVariable;
});
updatedData.data.variables = updatedVariables;
}
return updatedData;
};
// As we do not have order and ID's in the variables object, we have to process variables to add order and ID if they do not exist in the variables object
// eslint-disable-next-line sonarjs/cognitive-complexity
const transformDashboardVariables = (data: Dashboard): Dashboard => {
if (data && data.data && data.data.variables) {
const clonedDashboardData = mergeDBWithLocalStorage(
JSON.parse(JSON.stringify(data)),
currentDashboard,
);
const { variables } = clonedDashboardData.data;
const existingOrders: Set<number> = new Set();
for (const key in variables) {
// eslint-disable-next-line no-prototype-builtins
if (variables.hasOwnProperty(key)) {
const variable: IDashboardVariable = variables[key];
// Check if 'order' property doesn't exist or is undefined
if (variable.order === undefined) {
// Find a unique order starting from 0
let order = 0;
while (existingOrders.has(order)) {
order += 1;
}
variable.order = order;
existingOrders.add(order);
// ! BWC - Specific case for backward compatibility where textboxValue was used instead of defaultValue
if (variable.type === 'TEXTBOX' && !variable.defaultValue) {
variable.defaultValue = variable.textboxValue || '';
}
}
if (variable.id === undefined) {
variable.id = generateUUID();
}
}
}
return clonedDashboardData;
}
return data;
};
return {
transformDashboardVariables,
getUrlVariables,
updateUrlVariable,
currentDashboard,
updateLocalStorageDashboardVariables,
};
}

View File

@@ -11,7 +11,7 @@ export interface LocalStoreDashboardVariables {
| IDashboardVariable['selectedValue'];
}
interface UseVariablesFromUrlReturn {
export interface UseVariablesFromUrlReturn {
getUrlVariables: () => LocalStoreDashboardVariables;
setUrlVariables: (variables: LocalStoreDashboardVariables) => void;
updateUrlVariable: (

View File

@@ -107,7 +107,6 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
queryRangeMutation,
dashboardVariables,
dashboardDynamicVariables,
selectedDashboard?.data.version,
widget,
]);
};

View File

@@ -1,3 +1,16 @@
import { useParams } from 'react-router-dom';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import DashboardPage from './DashboardPage';
export default DashboardPage;
function DashboardPageWithProvider(): JSX.Element {
const { dashboardId } = useParams<{ dashboardId: string }>();
return (
<DashboardProvider dashboardId={dashboardId}>
<DashboardPage />
</DashboardProvider>
);
}
export default DashboardPageWithProvider;

View File

@@ -0,0 +1,146 @@
import { Route } from 'react-router-dom';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { rest, server } from 'mocks-server/server';
import { render, screen, waitFor } from 'tests/test-utils';
import DashboardWidget from '../index';
const DASHBOARD_ID = 'dash-1';
const WIDGET_ID = 'widget-abc';
const mockDashboardResponse = {
status: 'success',
data: {
id: DASHBOARD_ID,
createdAt: '2024-01-01T00:00:00Z',
createdBy: 'test',
updatedAt: '2024-01-01T00:00:00Z',
updatedBy: 'test',
isLocked: false,
data: {
collapsableRowsMigrated: true,
description: '',
name: '',
panelMap: {},
tags: [],
title: 'Test Dashboard',
uploadedGrafana: false,
uuid: '',
version: '',
variables: {},
widgets: [],
layout: [],
},
},
};
const mockSafeNavigate = jest.fn();
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
safeNavigate: mockSafeNavigate,
}),
}));
jest.mock('container/NewWidget', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="new-widget">NewWidget</div>,
}));
// nuqs's useQueryState doesn't read from MemoryRouter, so we mock it to return
// controlled values via the `mockQueryState` map below.
const mockQueryState: Record<string, string | null> = {};
jest.mock('nuqs', () => ({
...jest.requireActual('nuqs'),
useQueryState: (key: string): [string | null, jest.Mock] => [
mockQueryState[key] ?? null,
jest.fn(),
],
}));
// Wrap component in a Route so useParams can resolve dashboardId
function renderAtRoute(
queryState: Record<string, string | null> = {},
): ReturnType<typeof render> {
Object.assign(mockQueryState, queryState);
return render(
<Route path="/dashboard/:dashboardId/new">
<DashboardWidget />
</Route>,
undefined,
{ initialRoute: `/dashboard/${DASHBOARD_ID}/new` },
);
}
beforeEach(() => {
mockSafeNavigate.mockClear();
Object.keys(mockQueryState).forEach((k) => delete mockQueryState[k]);
});
describe('DashboardWidget', () => {
it('redirects to dashboard when widgetId is missing', async () => {
renderAtRoute({ graphType: PANEL_TYPES.TIME_SERIES });
await waitFor(() => {
expect(mockSafeNavigate).toHaveBeenCalled();
});
const [navigatedTo] = mockSafeNavigate.mock.calls[0];
expect(navigatedTo).toContain(`/dashboard/${DASHBOARD_ID}`);
});
it('redirects to dashboard when graphType is missing', async () => {
renderAtRoute({ widgetId: WIDGET_ID });
await waitFor(() => {
expect(mockSafeNavigate).toHaveBeenCalled();
});
const [navigatedTo] = mockSafeNavigate.mock.calls[0];
expect(navigatedTo).toContain(`/dashboard/${DASHBOARD_ID}`);
});
it('shows spinner while dashboard is loading', () => {
server.use(
rest.get(
`http://localhost/api/v1/dashboards/${DASHBOARD_ID}`,
(_req, res, ctx) => res(ctx.delay('infinite')),
),
);
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
expect(screen.getByRole('img', { name: 'loading' })).toBeInTheDocument();
});
it('shows error message when dashboard fetch fails', async () => {
server.use(
rest.get(
`http://localhost/api/v1/dashboards/${DASHBOARD_ID}`,
(_req, res, ctx) => res(ctx.status(500), ctx.json({ status: 'error' })),
),
);
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
await waitFor(() => {
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
});
});
it('renders NewWidget when dashboard loads successfully', async () => {
server.use(
rest.get(
`http://localhost/api/v1/dashboards/${DASHBOARD_ID}`,
(_req, res, ctx) => res(ctx.status(200), ctx.json(mockDashboardResponse)),
),
);
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
await waitFor(() => {
expect(screen.getByTestId('new-widget')).toBeInTheDocument();
});
});
});

View File

@@ -1,50 +1,98 @@
import { useEffect, useState } from 'react';
import { generatePath, useLocation, useParams } from 'react-router-dom';
import { useQuery } from 'react-query';
import { generatePath, useParams } from 'react-router-dom';
import { Card, Typography } from 'antd';
import getDashboard from 'api/v1/dashboards/id/get';
import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { DASHBOARD_CACHE_TIME } from 'constants/queryCacheTime';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
import NewWidget from 'container/NewWidget';
import { isDrilldownEnabled } from 'container/QueryTable/Drilldown/drilldownUtils';
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { Widgets } from 'types/api/dashboard/getAll';
import { parseAsStringEnum, useQueryState } from 'nuqs';
import { setDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import { Dashboard } from 'types/api/dashboard/getAll';
function DashboardWidget(): JSX.Element | null {
const { search } = useLocation();
const { dashboardId } = useParams<DashboardWidgetPageParams>();
const { dashboardId } = useParams<{
dashboardId: string;
}>();
const [widgetId] = useQueryState('widgetId');
const [graphType] = useQueryState(
'graphType',
parseAsStringEnum<PANEL_TYPES>(Object.values(PANEL_TYPES)),
);
const { safeNavigate } = useSafeNavigate();
const [selectedGraph, setSelectedGraph] = useState<PANEL_TYPES>();
const { selectedDashboard, dashboardResponse } = useDashboard();
const params = useUrlQuery();
const widgetId = params.get('widgetId');
const { data } = selectedDashboard || {};
const { widgets } = data || {};
const selectedWidget = widgets?.find((e) => e.id === widgetId) as Widgets;
useEffect(() => {
const params = new URLSearchParams(search);
const graphType = params.get('graphType') as PANEL_TYPES | null;
if (graphType === null) {
if (!graphType || !widgetId) {
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
} else {
setSelectedGraph(graphType);
} else if (!dashboardId) {
safeNavigate(ROUTES.HOME);
}
}, [dashboardId, safeNavigate, search]);
}, [graphType, widgetId, dashboardId, safeNavigate]);
if (selectedGraph === undefined || dashboardResponse.isLoading) {
if (!widgetId || !graphType) {
return null;
}
return (
<DashboardWidgetInternal
dashboardId={dashboardId}
widgetId={widgetId}
graphType={graphType}
/>
);
}
function DashboardWidgetInternal({
dashboardId,
widgetId,
graphType,
}: {
dashboardId: string;
widgetId: string;
graphType: PANEL_TYPES;
}): JSX.Element | null {
const [selectedDashboard, setSelectedDashboard] = useState<
Dashboard | undefined
>(undefined);
const { transformDashboardVariables } = useTransformDashboardVariables(
dashboardId,
);
const {
isFetching: isFetchingDashboardResponse,
isError: isErrorDashboardResponse,
} = useQuery([REACT_QUERY_KEY.DASHBOARD_BY_ID, dashboardId, widgetId], {
enabled: true,
queryFn: async () =>
await getDashboard({
id: dashboardId,
}),
refetchOnWindowFocus: false,
cacheTime: DASHBOARD_CACHE_TIME,
onSuccess: (response) => {
const updatedDashboardData = transformDashboardVariables(response.data);
setSelectedDashboard(updatedDashboardData);
setDashboardVariablesStore({
dashboardId,
variables: updatedDashboardData.data.variables,
});
},
});
if (isFetchingDashboardResponse) {
return <Spinner tip="Loading.." />;
}
if (dashboardResponse.isError) {
if (isErrorDashboardResponse) {
return (
<Card>
<Typography>{SOMETHING_WENT_WRONG}</Typography>
@@ -54,16 +102,11 @@ function DashboardWidget(): JSX.Element | null {
return (
<NewWidget
yAxisUnit={selectedWidget?.yAxisUnit}
selectedGraph={selectedGraph}
fillSpans={selectedWidget?.fillSpans}
dashboardId={dashboardId}
selectedGraph={graphType}
enableDrillDown={isDrilldownEnabled()}
selectedDashboard={selectedDashboard}
/>
);
}
export interface DashboardWidgetPageParams {
dashboardId: string;
}
export default DashboardWidget;

View File

@@ -8,7 +8,6 @@ import {
} from 'mocks-server/__mockdata__/dashboards';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { fireEvent, render, waitFor } from 'tests/test-utils';
jest.mock('container/DashboardContainer/DashboardDescription/utils', () => ({
@@ -19,11 +18,6 @@ jest.mock('container/DashboardContainer/DashboardDescription/utils', () => ({
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
useRouteMatch: jest.fn().mockReturnValue({
params: {
dashboardId: 4,
},
}),
}));
const mockWindowOpen = jest.fn();
@@ -47,9 +41,7 @@ describe('dashboard list page', () => {
<MemoryRouter
initialEntries={['/dashbords?columnKey=asgard&order=stones&page=1']}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
<DashboardsList />
</MemoryRouter>,
);
@@ -71,9 +63,7 @@ describe('dashboard list page', () => {
<MemoryRouter
initialEntries={['/dashbords?columnKey=createdAt&order=descend&page=1']}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
<DashboardsList />
</MemoryRouter>,
);
@@ -92,9 +82,7 @@ describe('dashboard list page', () => {
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
]}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
<DashboardsList />
</MemoryRouter>,
);
@@ -135,9 +123,7 @@ describe('dashboard list page', () => {
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
]}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
<DashboardsList />
</MemoryRouter>,
);
@@ -164,9 +150,7 @@ describe('dashboard list page', () => {
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
]}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
<DashboardsList />
</MemoryRouter>,
);
@@ -196,9 +180,7 @@ describe('dashboard list page', () => {
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
]}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
<DashboardsList />
</MemoryRouter>,
);

View File

@@ -63,6 +63,7 @@ function SettingsPage(): JSX.Element {
isAdmin &&
(item.key === ROUTES.BILLING ||
item.key === ROUTES.ORG_SETTINGS ||
item.key === ROUTES.MEMBERS_SETTINGS ||
item.key === ROUTES.MY_SETTINGS ||
item.key === ROUTES.SHORTCUTS)
),

View File

@@ -36,6 +36,7 @@ export const getRoutes = (
if (isWorkspaceBlocked && isAdmin) {
settings.push(
...organizationSettings(t),
...membersSettings(t),
...mySettings(t),
...billingSettings(t),
...keyboardShortcuts(t),

View File

@@ -14,26 +14,21 @@ import { useTranslation } from 'react-i18next';
import { useMutation, useQuery, UseQueryResult } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { useRouteMatch } from 'react-router-dom';
import { Modal } from 'antd';
import getDashboard from 'api/v1/dashboards/id/get';
import locked from 'api/v1/dashboards/id/lock';
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
import dayjs, { Dayjs } from 'dayjs';
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
import useTabVisibility from 'hooks/useTabFocus';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
import { defaultTo, isEmpty } from 'lodash-es';
import { defaultTo } from 'lodash-es';
import isEqual from 'lodash-es/isEqual';
import isUndefined from 'lodash-es/isUndefined';
import omitBy from 'lodash-es/omitBy';
import { useAppContext } from 'providers/App/App';
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
import { normalizeUrlValueForVariable } from 'providers/Dashboard/normalizeUrlValue';
import { useErrorModal } from 'providers/ErrorModalProvider';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
@@ -41,10 +36,9 @@ import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
import { SuccessResponseV2 } from 'types/api';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import { Dashboard } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as generateUUID } from 'uuid';
import {
DASHBOARD_CACHE_TIME,
@@ -75,8 +69,6 @@ export const DashboardContext = createContext<IDashboardContext>({
setLayouts: () => {},
setSelectedDashboard: () => {},
updatedTimeRef: {} as React.MutableRefObject<Dayjs | null>,
toScrollWidgetId: '',
setToScrollWidgetId: () => {},
updateLocalStorageDashboardVariables: () => {},
dashboardQueryRangeCalled: false,
setDashboardQueryRangeCalled: () => {},
@@ -85,18 +77,13 @@ export const DashboardContext = createContext<IDashboardContext>({
setColumnWidths: () => {},
});
interface Props {
dashboardId: string;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
export function DashboardProvider({
children,
}: PropsWithChildren): JSX.Element {
dashboardId,
}: PropsWithChildren<{ dashboardId: string }>): JSX.Element {
const [isDashboardSliderOpen, setIsDashboardSlider] = useState<boolean>(false);
const [toScrollWidgetId, setToScrollWidgetId] = useState<string>('');
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
const [
@@ -104,11 +91,6 @@ export function DashboardProvider({
setDashboardQueryRangeCalled,
] = useState<boolean>(false);
const isDashboardPage = useRouteMatch<Props>({
path: ROUTES.DASHBOARD,
exact: true,
});
const { showErrorModal } = useErrorModal();
const dispatch = useDispatch<Dispatch<AppActions>>();
@@ -119,11 +101,6 @@ export function DashboardProvider({
const [onModal, Content] = Modal.useModal();
const isDashboardWidgetPage = useRouteMatch<Props>({
path: ROUTES.DASHBOARD_WIDGET,
exact: true,
});
const [layouts, setLayouts] = useState<Layout[]>([]);
const [panelMap, setPanelMap] = useState<
@@ -132,11 +109,6 @@ export function DashboardProvider({
const { isLoggedIn } = useAppContext();
const dashboardId =
(isDashboardPage
? isDashboardPage.params.dashboardId
: isDashboardWidgetPage?.params.dashboardId) || '';
const [selectedDashboard, setSelectedDashboard] = useState<Dashboard>();
const dashboardVariables = useDashboardVariablesSelector((s) => s.variables);
const savedDashboardId = useDashboardVariablesSelector((s) => s.dashboardId);
@@ -161,9 +133,10 @@ export function DashboardProvider({
const {
currentDashboard,
updateLocalStorageDashboardVariables,
} = useDashboardVariablesFromLocalStorage(dashboardId);
const { getUrlVariables, updateUrlVariable } = useVariablesFromUrl();
getUrlVariables,
updateUrlVariable,
transformDashboardVariables,
} = useTransformDashboardVariables(dashboardId);
const updatedTimeRef = useRef<Dayjs | null>(null); // Using ref to store the updated time
const modalRef = useRef<any>(null);
@@ -175,108 +148,14 @@ export function DashboardProvider({
const [isDashboardFetching, setIsDashboardFetching] = useState<boolean>(false);
const mergeDBWithLocalStorage = (
data: Dashboard,
localStorageVariables: any,
): Dashboard => {
const updatedData = data;
if (data && localStorageVariables) {
const updatedVariables = data.data.variables;
const variablesFromUrl = getUrlVariables();
Object.keys(data.data.variables).forEach((variable) => {
const variableData = data.data.variables[variable];
// values from url
const urlVariable = variableData?.name
? variablesFromUrl[variableData?.name] || variablesFromUrl[variableData.id]
: variablesFromUrl[variableData.id];
let updatedVariable = {
...data.data.variables[variable],
...localStorageVariables[variableData.name as any],
};
// respect the url variable if it is set, override the others
if (!isEmpty(urlVariable)) {
if (urlVariable === ALL_SELECTED_VALUE) {
updatedVariable = {
...updatedVariable,
allSelected: true,
};
} else {
// Normalize URL value to match variable's multiSelect configuration
const normalizedValue = normalizeUrlValueForVariable(
urlVariable,
variableData,
);
updatedVariable = {
...updatedVariable,
selectedValue: normalizedValue,
// Only set allSelected to false if showALLOption is available
...(updatedVariable?.showALLOption && { allSelected: false }),
};
}
}
updatedVariables[variable] = updatedVariable;
});
updatedData.data.variables = updatedVariables;
}
return updatedData;
};
// As we do not have order and ID's in the variables object, we have to process variables to add order and ID if they do not exist in the variables object
// eslint-disable-next-line sonarjs/cognitive-complexity
const transformDashboardVariables = (data: Dashboard): Dashboard => {
if (data && data.data && data.data.variables) {
const clonedDashboardData = mergeDBWithLocalStorage(
JSON.parse(JSON.stringify(data)),
currentDashboard,
);
const { variables } = clonedDashboardData.data;
const existingOrders: Set<number> = new Set();
for (const key in variables) {
// eslint-disable-next-line no-prototype-builtins
if (variables.hasOwnProperty(key)) {
const variable: IDashboardVariable = variables[key];
// Check if 'order' property doesn't exist or is undefined
if (variable.order === undefined) {
// Find a unique order starting from 0
let order = 0;
while (existingOrders.has(order)) {
order += 1;
}
variable.order = order;
existingOrders.add(order);
// ! BWC - Specific case for backward compatibility where textboxValue was used instead of defaultValue
if (variable.type === 'TEXTBOX' && !variable.defaultValue) {
variable.defaultValue = variable.textboxValue || '';
}
}
if (variable.id === undefined) {
variable.id = generateUUID();
}
}
}
return clonedDashboardData;
}
return data;
};
const dashboardResponse = useQuery(
[
REACT_QUERY_KEY.DASHBOARD_BY_ID,
isDashboardPage?.params,
dashboardId,
globalTime.isAutoRefreshDisabled,
],
{
enabled: (!!isDashboardPage || !!isDashboardWidgetPage) && isLoggedIn,
enabled: !!dashboardId && isLoggedIn,
queryFn: async () => {
setIsDashboardFetching(true);
try {
@@ -299,13 +178,14 @@ export function DashboardProvider({
},
onSuccess: (data: SuccessResponseV2<Dashboard>) => {
// if the url variable is not set for any variable, set it to the default value
const variables = data?.data?.data?.variables;
const updatedDashboardData = transformDashboardVariables(data?.data);
// initialize URL variables after dashboard state is set to avoid race conditions
const variables = updatedDashboardData?.data?.variables;
if (variables) {
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
}
const updatedDashboardData = transformDashboardVariables(data?.data);
const updatedDate = dayjs(updatedDashboardData?.updatedAt);
setIsDashboardLocked(updatedDashboardData?.locked || false);
@@ -396,11 +276,7 @@ export function DashboardProvider({
useEffect(() => {
// make the call on tab visibility only if the user is on dashboard / widget page
if (
isVisible &&
updatedTimeRef.current &&
(!!isDashboardPage || !!isDashboardWidgetPage)
) {
if (isVisible && updatedTimeRef.current && !!dashboardId) {
dashboardResponse.refetch();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -443,7 +319,6 @@ export function DashboardProvider({
const value: IDashboardContext = useMemo(
() => ({
toScrollWidgetId,
isDashboardSliderOpen,
isDashboardLocked,
handleToggleDashboardSlider,
@@ -457,7 +332,6 @@ export function DashboardProvider({
setPanelMap,
setSelectedDashboard,
updatedTimeRef,
setToScrollWidgetId,
updateLocalStorageDashboardVariables,
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
@@ -474,7 +348,6 @@ export function DashboardProvider({
dashboardId,
layouts,
panelMap,
toScrollWidgetId,
updateLocalStorageDashboardVariables,
currentDashboard,
dashboardQueryRangeCalled,

View File

@@ -2,11 +2,10 @@ import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { render, screen, waitFor } from '@testing-library/react';
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
import getDashboard from 'api/v1/dashboards/id/get';
import { DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED } from 'constants/queryCacheTime';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
import { DashboardProvider, useDashboard } from 'providers/Dashboard/Dashboard';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
@@ -19,30 +18,28 @@ jest.mock('api/v1/dashboards/id/get');
jest.mock('api/v1/dashboards/id/lock');
const mockGetDashboard = jest.mocked(getDashboard);
// Mock useRouteMatch to simulate different route scenarios
const mockUseRouteMatch = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useRouteMatch: (): any => mockUseRouteMatch(),
}));
// Mock other dependencies
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
safeNavigate: jest.fn(),
}),
}));
// Mock only the essential dependencies for Dashboard provider
jest.mock('providers/App/App', () => ({
useAppContext: (): any => ({
useAppContext: (): {
isLoggedIn: boolean;
user: { email: string; role: string };
} => ({
isLoggedIn: true,
user: { email: 'test@example.com', role: 'ADMIN' },
}),
}));
jest.mock('providers/ErrorModalProvider', () => ({
useErrorModal: (): any => ({ showErrorModal: jest.fn() }),
useErrorModal: (): { showErrorModal: jest.Mock } => ({
showErrorModal: jest.fn(),
}),
}));
jest.mock('react-redux', () => ({
@@ -60,11 +57,10 @@ jest.mock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid') }));
function TestComponent(): JSX.Element {
const { dashboardResponse, selectedDashboard } = useDashboard();
const { dashboardVariables } = useDashboardVariables();
const dashboardId = selectedDashboard?.id;
return (
<div>
<div data-testid="dashboard-id">{dashboardId}</div>
<div data-testid="dashboard-id">{selectedDashboard?.id}</div>
<div data-testid="query-status">{dashboardResponse.status}</div>
<div data-testid="is-loading">{dashboardResponse.isLoading.toString()}</div>
<div data-testid="is-fetching">
@@ -94,27 +90,15 @@ function createTestQueryClient(): QueryClient {
// Helper to render with dashboard provider
function renderWithDashboardProvider(
initialRoute = '/dashboard/test-dashboard-id',
routeMatchParams?: { dashboardId: string } | null,
): any {
dashboardId = 'test-dashboard-id',
): RenderResult {
const queryClient = createTestQueryClient();
// Mock the route match
mockUseRouteMatch.mockReturnValue(
routeMatchParams
? {
path: ROUTES.DASHBOARD,
url: `/dashboard/${routeMatchParams.dashboardId}`,
isExact: true,
params: routeMatchParams,
}
: null,
);
const initialRoute = dashboardId ? `/dashboard/${dashboardId}` : '/dashboard';
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[initialRoute]}>
<DashboardProvider>
<DashboardProvider dashboardId={dashboardId}>
<TestComponent />
</DashboardProvider>
</MemoryRouter>
@@ -188,7 +172,7 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
describe('Query Key Behavior', () => {
it('should include route params in query key when on dashboard page', async () => {
const dashboardId = 'test-dashboard-id';
renderWithDashboardProvider(`/dashboard/${dashboardId}`, { dashboardId });
renderWithDashboardProvider(dashboardId);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: dashboardId });
@@ -203,30 +187,17 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
const newDashboardId = 'new-dashboard-id';
// First render with initial dashboard ID
const { rerender } = renderWithDashboardProvider(
`/dashboard/${initialDashboardId}`,
{
dashboardId: initialDashboardId,
},
);
const { rerender } = renderWithDashboardProvider(initialDashboardId);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: initialDashboardId });
});
// Change route params to simulate navigation
mockUseRouteMatch.mockReturnValue({
path: ROUTES.DASHBOARD,
url: `/dashboard/${newDashboardId}`,
isExact: true,
params: { dashboardId: newDashboardId },
});
// Rerender with new route
// Rerender with new dashboard ID prop
rerender(
<QueryClientProvider client={createTestQueryClient()}>
<MemoryRouter initialEntries={[`/dashboard/${newDashboardId}`]}>
<DashboardProvider>
<DashboardProvider dashboardId={newDashboardId}>
<TestComponent />
</DashboardProvider>
</MemoryRouter>
@@ -241,50 +212,24 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
expect(mockGetDashboard).toHaveBeenCalledTimes(2);
});
it('should not fetch when not on dashboard page', () => {
// Mock no route match (not on dashboard page)
mockUseRouteMatch.mockReturnValue(null);
renderWithDashboardProvider('/some-other-page', null);
it('should not fetch when no dashboardId is provided', () => {
renderWithDashboardProvider('');
// Should not call the API
expect(mockGetDashboard).not.toHaveBeenCalled();
});
it('should handle undefined route params gracefully', async () => {
// Mock route match with undefined params
mockUseRouteMatch.mockReturnValue({
path: ROUTES.DASHBOARD,
url: '/dashboard/undefined',
isExact: true,
params: undefined,
});
renderWithDashboardProvider('/dashboard/undefined');
// Should not call API when params are undefined
expect(mockGetDashboard).not.toHaveBeenCalled();
});
});
describe('Cache Behavior', () => {
it('should create separate cache entries for different route params', async () => {
it('should create separate cache entries for different dashboardIds', async () => {
const queryClient = createTestQueryClient();
const dashboardId1 = 'dashboard-1';
const dashboardId2 = 'dashboard-2';
// First dashboard
mockUseRouteMatch.mockReturnValue({
path: ROUTES.DASHBOARD,
url: `/dashboard/${dashboardId1}`,
isExact: true,
params: { dashboardId: dashboardId1 },
});
const { rerender } = render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[`/dashboard/${dashboardId1}`]}>
<DashboardProvider>
<DashboardProvider dashboardId={dashboardId1}>
<TestComponent />
</DashboardProvider>
</MemoryRouter>
@@ -295,18 +240,10 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: dashboardId1 });
});
// Second dashboard
mockUseRouteMatch.mockReturnValue({
path: ROUTES.DASHBOARD,
url: `/dashboard/${dashboardId2}`,
isExact: true,
params: { dashboardId: dashboardId2 },
});
rerender(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[`/dashboard/${dashboardId2}`]}>
<DashboardProvider>
<DashboardProvider dashboardId={dashboardId2}>
<TestComponent />
</DashboardProvider>
</MemoryRouter>
@@ -325,13 +262,11 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
expect(cacheKeys).toHaveLength(2);
expect(cacheKeys[0]).toEqual([
REACT_QUERY_KEY.DASHBOARD_BY_ID,
{ dashboardId: dashboardId1 },
dashboardId1,
true, // globalTime.isAutoRefreshDisabled
]);
expect(cacheKeys[1]).toEqual([
REACT_QUERY_KEY.DASHBOARD_BY_ID,
{ dashboardId: dashboardId2 },
dashboardId2,
true, // globalTime.isAutoRefreshDisabled
]);
@@ -348,17 +283,10 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
const queryClient = createTestQueryClient();
const dashboardId = 'auto-refresh-dashboard';
mockUseRouteMatch.mockReturnValue({
path: ROUTES.DASHBOARD,
url: `/dashboard/${dashboardId}`,
isExact: true,
params: { dashboardId },
});
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[`/dashboard/${dashboardId}`]}>
<DashboardProvider>
<DashboardProvider dashboardId={dashboardId}>
<TestComponent />
</DashboardProvider>
</MemoryRouter>
@@ -375,7 +303,7 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
.find(
(query) =>
query.queryKey[0] === REACT_QUERY_KEY.DASHBOARD_BY_ID &&
query.queryKey[3] === false,
query.queryKey[2] === false,
);
expect(dashboardQuery).toBeDefined();
expect((dashboardQuery as { cacheTime: number }).cacheTime).toBe(
@@ -437,9 +365,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
// Empty URL variables - tests initialization flow
mockGetUrlVariables.mockReturnValue({});
renderWithDashboardProvider(`/dashboard/${DASHBOARD_ID}`, {
dashboardId: DASHBOARD_ID,
});
renderWithDashboardProvider(DASHBOARD_ID);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
@@ -455,6 +381,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
multiSelect: false,
allSelected: false,
showALLOption: true,
order: 0,
},
services: {
id: 'svc-id',
@@ -462,6 +389,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
multiSelect: true,
allSelected: false,
showALLOption: true,
order: 1,
},
},
mockGetUrlVariables,
@@ -493,9 +421,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
.mockReturnValueOnce('development')
.mockReturnValueOnce(['db', 'cache']);
renderWithDashboardProvider(`/dashboard/${DASHBOARD_ID}`, {
dashboardId: DASHBOARD_ID,
});
renderWithDashboardProvider(DASHBOARD_ID);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
@@ -555,9 +481,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
mockGetUrlVariables.mockReturnValue(urlVariables);
renderWithDashboardProvider(`/dashboard/${DASHBOARD_ID}`, {
dashboardId: DASHBOARD_ID,
});
renderWithDashboardProvider(DASHBOARD_ID);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
@@ -593,9 +517,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
.mockReturnValueOnce('development')
.mockReturnValueOnce(['api']);
renderWithDashboardProvider(`/dashboard/${DASHBOARD_ID}`, {
dashboardId: DASHBOARD_ID,
});
renderWithDashboardProvider(DASHBOARD_ID);
await waitFor(() => {
// Verify normalization was called with the specific values and variable configs
@@ -662,9 +584,7 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
} as any);
/* eslint-enable @typescript-eslint/no-explicit-any */
renderWithDashboardProvider(`/dashboard/${DASHBOARD_ID}`, {
dashboardId: DASHBOARD_ID,
});
renderWithDashboardProvider(DASHBOARD_ID);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
@@ -706,9 +626,7 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
} as any);
/* eslint-enable @typescript-eslint/no-explicit-any */
renderWithDashboardProvider(`/dashboard/${DASHBOARD_ID}`, {
dashboardId: DASHBOARD_ID,
});
renderWithDashboardProvider(DASHBOARD_ID);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
@@ -751,9 +669,7 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
} as any);
/* eslint-enable @typescript-eslint/no-explicit-any */
renderWithDashboardProvider(`/dashboard/${DASHBOARD_ID}`, {
dashboardId: DASHBOARD_ID,
});
renderWithDashboardProvider(DASHBOARD_ID);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
@@ -795,9 +711,7 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
} as any);
/* eslint-enable @typescript-eslint/no-explicit-any */
renderWithDashboardProvider(`/dashboard/${DASHBOARD_ID}`, {
dashboardId: DASHBOARD_ID,
});
renderWithDashboardProvider(DASHBOARD_ID);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });

View File

@@ -0,0 +1,13 @@
import { create } from 'zustand';
interface ScrollToWidgetIdState {
toScrollWidgetId: string;
setToScrollWidgetId: (widgetId: string) => void;
}
export const useScrollToWidgetIdStore = create<ScrollToWidgetIdState>(
(set) => ({
toScrollWidgetId: '',
setToScrollWidgetId: (widgetId): void => set({ toScrollWidgetId: widgetId }),
}),
);

View File

@@ -23,8 +23,6 @@ export interface IDashboardContext {
React.SetStateAction<Dashboard | undefined>
>;
updatedTimeRef: React.MutableRefObject<dayjs.Dayjs | null>;
toScrollWidgetId: string;
setToScrollWidgetId: React.Dispatch<React.SetStateAction<string>>;
updateLocalStorageDashboardVariables: (
id: string,
selectedValue:

View File

@@ -0,0 +1,9 @@
/**
* Masks a key string, showing only the first 2 and last 2 characters.
*/
export function getMaskedKey(key: string): string {
if (!key || key.length < 4) {
return key || 'N/A';
}
return `${key.substring(0, 2)}·······${key.slice(-2).trim()}`;
}

2
go.mod
View File

@@ -11,7 +11,6 @@ require (
github.com/SigNoz/signoz-otel-collector v0.144.2
github.com/antlr4-go/antlr/v4 v4.13.1
github.com/antonmedv/expr v1.15.3
github.com/bytedance/sonic v1.14.1
github.com/cespare/xxhash/v2 v2.3.0
github.com/coreos/go-oidc/v3 v3.17.0
github.com/dgraph-io/ristretto/v2 v2.3.0
@@ -106,6 +105,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect

View File

@@ -72,7 +72,7 @@ func (service *Service) SyncServers(ctx context.Context) error {
service.serversMtx.Lock()
for _, org := range orgs {
config, err := service.getConfig(ctx, org.ID.StringValue())
config, _, err := service.getConfig(ctx, org.ID.StringValue())
if err != nil {
service.settings.Logger().ErrorContext(ctx, "failed to get alertmanager config for org", "org_id", org.ID.StringValue(), "error", err)
continue
@@ -171,7 +171,7 @@ func (service *Service) Stop(ctx context.Context) error {
}
func (service *Service) newServer(ctx context.Context, orgID string) (*alertmanagerserver.Server, error) {
config, err := service.getConfig(ctx, orgID)
config, storedHash, err := service.getConfig(ctx, orgID)
if err != nil {
return nil, err
}
@@ -181,13 +181,16 @@ func (service *Service) newServer(ctx context.Context, orgID string) (*alertmana
return nil, err
}
beforeCompareAndSelectHash := config.StoreableConfig().Hash
config, err = service.compareAndSelectConfig(ctx, config)
if err != nil {
return nil, err
}
if beforeCompareAndSelectHash == config.StoreableConfig().Hash {
// compare against the hash of the config stored in the DB (before overlays
// were applied by getConfig). This ensures that overlay changes (e.g. new
// defaults from an upstream upgrade or something similar) trigger a DB update
// so that other code paths reading directly from the store see the up-to-date config.
if storedHash == config.StoreableConfig().Hash {
service.settings.Logger().DebugContext(ctx, "skipping config store update for org", "org_id", orgID, "hash", config.StoreableConfig().Hash)
return server, nil
}
@@ -200,27 +203,33 @@ func (service *Service) newServer(ctx context.Context, orgID string) (*alertmana
return server, nil
}
func (service *Service) getConfig(ctx context.Context, orgID string) (*alertmanagertypes.Config, error) {
// getConfig returns the config for the given orgID with overlays applied, along
// with the hash that was stored in the DB before overlays. When no config exists
// in the store yet the stored hash is empty.
func (service *Service) getConfig(ctx context.Context, orgID string) (*alertmanagertypes.Config, string, error) {
config, err := service.configStore.Get(ctx, orgID)
var storedHash string
if err != nil {
if !errors.Ast(err, errors.TypeNotFound) {
return nil, err
return nil, "", err
}
config, err = alertmanagertypes.NewDefaultConfig(service.config.Global, service.config.Route, orgID)
if err != nil {
return nil, err
return nil, "", err
}
} else {
storedHash = config.StoreableConfig().Hash
}
if err := config.SetGlobalConfig(service.config.Global); err != nil {
return nil, err
return nil, "", err
}
if err := config.SetRouteConfig(service.config.Route); err != nil {
return nil, err
return nil, "", err
}
return config, nil
return config, storedHash, nil
}
func (service *Service) compareAndSelectConfig(ctx context.Context, incomingConfig *alertmanagertypes.Config) (*alertmanagertypes.Config, error) {

View File

@@ -4,6 +4,7 @@ import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/gorilla/mux"
)
@@ -21,7 +22,7 @@ func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -38,7 +39,7 @@ func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -55,7 +56,7 @@ func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
@@ -72,7 +73,7 @@ func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -25,7 +25,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -42,7 +42,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -59,7 +59,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
@@ -76,7 +76,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/gorilla/mux"
)
@@ -23,7 +23,7 @@ func (provider *provider) addFieldsRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -41,7 +41,7 @@ func (provider *provider) addFieldsRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/gorilla/mux"
)
@@ -22,7 +22,7 @@ func (provider *provider) addFlaggerRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/gatewaytypes"
"github.com/gorilla/mux"
)
@@ -23,7 +23,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -41,7 +41,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -58,7 +58,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -75,7 +75,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
@@ -92,7 +92,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
@@ -109,7 +109,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -126,7 +126,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
@@ -143,7 +143,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -5,7 +5,6 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/gorilla/mux"
)
@@ -22,7 +21,7 @@ func (provider *provider) addGlobalRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleEditor),
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/metricsexplorertypes"
"github.com/gorilla/mux"
)
@@ -25,7 +25,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -44,7 +44,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -63,7 +63,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -83,7 +83,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -102,7 +102,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -121,7 +121,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleEditor),
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -140,7 +140,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -159,7 +159,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -178,7 +178,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}

View File

@@ -5,7 +5,6 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/gorilla/mux"
)
@@ -22,7 +21,7 @@ func (provider *provider) addOrgRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -39,7 +38,7 @@ func (provider *provider) addOrgRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusConflict, http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/preferencetypes"
"github.com/gorilla/mux"
)
@@ -22,7 +22,7 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -39,7 +39,7 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -56,7 +56,7 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
@@ -73,7 +73,7 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -90,7 +90,7 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -107,7 +107,7 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/promotetypes"
"github.com/gorilla/mux"
)
@@ -21,7 +21,7 @@ func (provider *provider) addPromoteRoutes(router *mux.Router) error {
ResponseContentType: "",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newSecuritySchemes(authtypes.RoleEditor),
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -37,7 +37,7 @@ func (provider *provider) addPromoteRoutes(router *mux.Router) error {
ResponseContentType: "",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}

View File

@@ -22,7 +22,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/zeus"
"github.com/gorilla/mux"
@@ -236,7 +236,7 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return nil
}
func newSecuritySchemes(role authtypes.LegacyRole) []handler.OpenAPISecurityScheme {
func newSecuritySchemes(role types.Role) []handler.OpenAPISecurityScheme {
return []handler.OpenAPISecurityScheme{
{Name: ctxtypes.AuthTypeAPIKey.StringValue(), Scopes: []string{role.String()}},
{Name: ctxtypes.AuthTypeTokenizer.StringValue(), Scopes: []string{role.String()}},

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/gorilla/mux"
)
@@ -446,7 +446,7 @@ func (provider *provider) addQuerierRoutes(router *mux.Router) error {
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -462,7 +462,7 @@ func (provider *provider) addQuerierRoutes(router *mux.Router) error {
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newSecuritySchemes(authtypes.RoleViewer),
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/gorilla/mux"
)
@@ -15,14 +16,14 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Tags: []string{"role"},
Summary: "Create role",
Description: "This endpoint creates a role",
Request: new(authtypes.PostableRole),
Request: new(roletypes.PostableRole),
RequestContentType: "",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -34,12 +35,12 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Description: "This endpoint lists all roles",
Request: nil,
RequestContentType: "",
Response: make([]*authtypes.Role, 0),
Response: make([]*roletypes.Role, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -51,12 +52,12 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Description: "This endpoint gets a role",
Request: nil,
RequestContentType: "",
Response: new(authtypes.Role),
Response: new(roletypes.Role),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -73,7 +74,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -83,14 +84,14 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Tags: []string{"role"},
Summary: "Patch role",
Description: "This endpoint patches a role",
Request: new(authtypes.PatchableRole),
Request: new(roletypes.PatchableRole),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
@@ -107,7 +108,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
@@ -124,7 +125,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -5,7 +5,6 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
"github.com/gorilla/mux"
)
@@ -23,7 +22,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -40,7 +39,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -57,7 +56,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -74,7 +73,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
@@ -91,7 +90,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
@@ -108,7 +107,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
@@ -125,7 +124,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -142,7 +141,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -159,7 +158,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
@@ -176,7 +175,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -4,9 +4,8 @@ import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/usertypes"
"github.com/gorilla/mux"
)
@@ -16,14 +15,14 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Tags: []string{"users"},
Summary: "Create invite",
Description: "This endpoint creates an invite for a user",
Request: new(usertypes.PostableInvite),
Request: new(types.PostableInvite),
RequestContentType: "application/json",
Response: new(usertypes.Invite),
Response: new(types.Invite),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -33,13 +32,13 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Tags: []string{"users"},
Summary: "Create bulk invite",
Description: "This endpoint creates a bulk invite for a user",
Request: new(usertypes.PostableBulkInviteRequest),
Request: new(types.PostableBulkInviteRequest),
RequestContentType: "application/json",
Response: nil,
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -51,7 +50,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Description: "This endpoint gets an invite by token",
Request: nil,
RequestContentType: "",
Response: new(usertypes.Invite),
Response: new(types.Invite),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
@@ -73,7 +72,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
@@ -85,12 +84,12 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Description: "This endpoint lists all invites",
Request: nil,
RequestContentType: "",
Response: make([]*usertypes.Invite, 0),
Response: make([]*types.Invite, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -100,9 +99,9 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Tags: []string{"users"},
Summary: "Accept invite",
Description: "This endpoint accepts an invite by token",
Request: new(usertypes.PostableAcceptInvite),
Request: new(types.PostableAcceptInvite),
RequestContentType: "application/json",
Response: new(usertypes.User),
Response: new(types.User),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
@@ -112,6 +111,74 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/pats", handler.New(provider.authZ.AdminAccess(provider.userHandler.CreateAPIKey), handler.OpenAPIDef{
ID: "CreateAPIKey",
Tags: []string{"users"},
Summary: "Create api key",
Description: "This endpoint creates an api key",
Request: new(types.PostableAPIKey),
RequestContentType: "application/json",
Response: new(types.GettableAPIKey),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/pats", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListAPIKeys), handler.OpenAPIDef{
ID: "ListAPIKeys",
Tags: []string{"users"},
Summary: "List api keys",
Description: "This endpoint lists all api keys",
Request: nil,
RequestContentType: "",
Response: make([]*types.GettableAPIKey, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/pats/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.UpdateAPIKey), handler.OpenAPIDef{
ID: "UpdateAPIKey",
Tags: []string{"users"},
Summary: "Update api key",
Description: "This endpoint updates an api key",
Request: new(types.StorableAPIKey),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/pats/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.RevokeAPIKey), handler.OpenAPIDef{
ID: "RevokeAPIKey",
Tags: []string{"users"},
Summary: "Revoke api key",
Description: "This endpoint revokes an api key",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/user", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListUsers), handler.OpenAPIDef{
ID: "ListUsers",
Tags: []string{"users"},
@@ -119,12 +186,12 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Description: "This endpoint lists all users",
Request: nil,
RequestContentType: "",
Response: make([]*usertypes.User, 0),
Response: make([]*types.GettableUser, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -136,7 +203,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Description: "This endpoint returns the user I belong to",
Request: nil,
RequestContentType: "",
Response: new(usertypes.User),
Response: new(types.GettableUser),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
@@ -153,12 +220,12 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Description: "This endpoint returns the user by id",
Request: nil,
RequestContentType: "",
Response: new(usertypes.User),
Response: new(types.GettableUser),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -168,14 +235,14 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Tags: []string{"users"},
Summary: "Update user",
Description: "This endpoint updates the user by id",
Request: new(usertypes.UpdatableUser),
Request: new(types.User),
RequestContentType: "application/json",
Response: new(usertypes.User),
Response: new(types.GettableUser),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
@@ -192,7 +259,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
@@ -204,12 +271,12 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Description: "This endpoint returns the reset password token by id",
Request: nil,
RequestContentType: "",
Response: new(usertypes.ResetPasswordToken),
Response: new(types.ResetPasswordToken),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -219,7 +286,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Tags: []string{"users"},
Summary: "Reset password",
Description: "This endpoint resets the password by token",
Request: new(usertypes.PostableResetPassword),
Request: new(types.PostableResetPassword),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
@@ -236,14 +303,14 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Tags: []string{"users"},
Summary: "Change password",
Description: "This endpoint changes the password by id",
Request: new(usertypes.ChangePasswordRequest),
Request: new(types.ChangePasswordRequest),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -253,7 +320,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Tags: []string{"users"},
Summary: "Forgot password",
Description: "This endpoint initiates the forgot password flow by sending a reset password email",
Request: new(usertypes.PostableForgotPassword),
Request: new(types.PostableForgotPassword),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
"github.com/gorilla/mux"
)
@@ -22,7 +22,7 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
@@ -39,7 +39,7 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -56,7 +56,7 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(authtypes.RoleAdmin),
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -16,6 +17,37 @@ func NewStore(sqlstore sqlstore.SQLStore) authtypes.AuthNStore {
return &store{sqlstore: sqlstore}
}
func (store *store) GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.User, *types.FactorPassword, error) {
user := new(types.User)
factorPassword := new(types.FactorPassword)
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(user).
Where("email = ?", email).
Where("org_id = ?", orgID).
Where("status = ?", types.UserStatusActive.StringValue()).
Scan(ctx)
if err != nil {
return nil, nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeUserNotFound, "user with email %s in org %s not found", email, orgID)
}
err = store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(factorPassword).
Where("user_id = ?", user.ID).
Scan(ctx)
if err != nil {
return nil, nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodePasswordNotFound, "user with email %s in org %s does not have password", email, orgID)
}
return user, factorPassword, nil
}
func (store *store) GetAuthDomainFromID(ctx context.Context, domainID valuer.UUID) (*authtypes.AuthDomain, error) {
storableAuthDomain := new(authtypes.StorableAuthDomain)

View File

@@ -5,31 +5,30 @@ import (
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/usertypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
var _ authn.PasswordAuthN = (*AuthN)(nil)
type AuthN struct {
store authtypes.AuthNStore
userStore usertypes.UserStore
store authtypes.AuthNStore
}
func New(store authtypes.AuthNStore, userStore usertypes.UserStore) *AuthN {
return &AuthN{store: store, userStore: userStore}
func New(store authtypes.AuthNStore) *AuthN {
return &AuthN{store: store}
}
func (a *AuthN) Authenticate(ctx context.Context, email string, password string, orgID valuer.UUID) (*authtypes.Identity, error) {
user, factorPassword, err := a.userStore.GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx, email, orgID)
user, factorPassword, err := a.store.GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx, email, orgID)
if err != nil {
return nil, err
}
if !factorPassword.Equals(password) {
return nil, errors.New(errors.TypeUnauthenticated, usertypes.ErrCodeIncorrectPassword, "invalid email or password")
return nil, errors.New(errors.TypeUnauthenticated, types.ErrCodeIncorrectPassword, "invalid email or password")
}
return authtypes.NewIdentity(user.ID, valuer.UUID{}, authtypes.PrincipalUser, orgID, valuer.MustNewEmail(user.Email)), nil
return authtypes.NewIdentity(user.ID, orgID, user.Email, user.Role), nil
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
@@ -29,10 +30,10 @@ type AuthZ interface {
ListObjects(context.Context, string, authtypes.Relation, authtypes.Typeable) ([]*authtypes.Object, error)
// Creates the role.
Create(context.Context, valuer.UUID, *authtypes.Role) error
Create(context.Context, valuer.UUID, *roletypes.Role) error
// Gets the role if it exists or creates one.
GetOrCreate(context.Context, valuer.UUID, *authtypes.Role) (*authtypes.Role, error)
GetOrCreate(context.Context, valuer.UUID, *roletypes.Role) (*roletypes.Role, error)
// Gets the objects associated with the given role and relation.
GetObjects(context.Context, valuer.UUID, valuer.UUID, authtypes.Relation) ([]*authtypes.Object, error)
@@ -41,7 +42,7 @@ type AuthZ interface {
GetResources(context.Context) []*authtypes.Resource
// Patches the role.
Patch(context.Context, valuer.UUID, *authtypes.Role) error
Patch(context.Context, valuer.UUID, *roletypes.Role) error
// Patches the objects in authorization server associated with the given role and relation
PatchObjects(context.Context, valuer.UUID, string, authtypes.Relation, []*authtypes.Object, []*authtypes.Object) error
@@ -50,19 +51,19 @@ type AuthZ interface {
Delete(context.Context, valuer.UUID, valuer.UUID) error
// Gets the role
Get(context.Context, valuer.UUID, valuer.UUID) (*authtypes.Role, error)
Get(context.Context, valuer.UUID, valuer.UUID) (*roletypes.Role, error)
// Gets the role by org_id and name
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*authtypes.Role, error)
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*roletypes.Role, error)
// Lists all the roles for the organization.
List(context.Context, valuer.UUID) ([]*authtypes.Role, error)
List(context.Context, valuer.UUID) ([]*roletypes.Role, error)
// Lists all the roles for the organization filtered by name
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*authtypes.Role, error)
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*roletypes.Role, error)
// Lists all the roles for the organization filtered by ids
ListByOrgIDAndIDs(context.Context, valuer.UUID, []valuer.UUID) ([]*authtypes.Role, error)
ListByOrgIDAndIDs(context.Context, valuer.UUID, []valuer.UUID) ([]*roletypes.Role, error)
// Grants a role to the subject based on role name.
Grant(context.Context, valuer.UUID, []string, string) error
@@ -74,7 +75,7 @@ type AuthZ interface {
ModifyGrant(context.Context, valuer.UUID, []string, []string, string) error
// Bootstrap the managed roles.
CreateManagedRoles(context.Context, valuer.UUID, []*authtypes.Role) error
CreateManagedRoles(context.Context, valuer.UUID, []*roletypes.Role) error
// Bootstrap managed roles transactions and user assignments
CreateManagedUserRoleTransactions(context.Context, valuer.UUID, valuer.UUID) error

View File

@@ -5,7 +5,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
@@ -14,11 +14,11 @@ type store struct {
sqlstore sqlstore.SQLStore
}
func NewSqlAuthzStore(sqlstore sqlstore.SQLStore) authtypes.RoleStore {
func NewSqlAuthzStore(sqlstore sqlstore.SQLStore) roletypes.Store {
return &store{sqlstore: sqlstore}
}
func (store *store) Create(ctx context.Context, role *authtypes.StorableRole) error {
func (store *store) Create(ctx context.Context, role *roletypes.StorableRole) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
@@ -32,8 +32,8 @@ func (store *store) Create(ctx context.Context, role *authtypes.StorableRole) er
return nil
}
func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.StorableRole, error) {
role := new(authtypes.StorableRole)
func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.StorableRole, error) {
role := new(roletypes.StorableRole)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -43,14 +43,14 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
Where("id = ?", id).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, authtypes.ErrCodeRoleNotFound, "role with id: %s doesn't exist", id)
return nil, store.sqlstore.WrapNotFoundErrf(err, roletypes.ErrCodeRoleNotFound, "role with id: %s doesn't exist", id)
}
return role, nil
}
func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.StorableRole, error) {
role := new(authtypes.StorableRole)
func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.StorableRole, error) {
role := new(roletypes.StorableRole)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -60,14 +60,14 @@ func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, na
Where("name = ?", name).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, authtypes.ErrCodeRoleNotFound, "role with name: %s doesn't exist", name)
return nil, store.sqlstore.WrapNotFoundErrf(err, roletypes.ErrCodeRoleNotFound, "role with name: %s doesn't exist", name)
}
return role, nil
}
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.StorableRole, error) {
roles := make([]*authtypes.StorableRole, 0)
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.StorableRole, error) {
roles := make([]*roletypes.StorableRole, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -82,8 +82,8 @@ func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.S
return roles, nil
}
func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.StorableRole, error) {
roles := make([]*authtypes.StorableRole, 0)
func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.StorableRole, error) {
roles := make([]*roletypes.StorableRole, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -97,14 +97,18 @@ func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID,
}
if len(roles) != len(names) {
return nil, errors.Newf(errors.TypeInvalidInput, authtypes.ErrCodeRoleNotFound, "not all roles found for the provided names: %v", names)
return nil, store.sqlstore.WrapNotFoundErrf(
nil,
roletypes.ErrCodeRoleNotFound,
"not all roles found for the provided names: %v", names,
)
}
return roles, nil
}
func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.StorableRole, error) {
roles := make([]*authtypes.StorableRole, 0)
func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*roletypes.StorableRole, error) {
roles := make([]*roletypes.StorableRole, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -118,13 +122,17 @@ func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, id
}
if len(roles) != len(ids) {
return nil, errors.Newf(errors.TypeInvalidInput, authtypes.ErrCodeRoleNotFound, "not all roles found for the provided ids: %v", ids)
return nil, store.sqlstore.WrapNotFoundErrf(
nil,
roletypes.ErrCodeRoleNotFound,
"not all roles found for the provided ids: %v", ids,
)
}
return roles, nil
}
func (store *store) Update(ctx context.Context, orgID valuer.UUID, role *authtypes.StorableRole) error {
func (store *store) Update(ctx context.Context, orgID valuer.UUID, role *roletypes.StorableRole) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
@@ -145,12 +153,12 @@ func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUI
sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(authtypes.StorableRole)).
Model(new(roletypes.StorableRole)).
Where("org_id = ?", orgID).
Where("id = ?", id).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapNotFoundErrf(err, authtypes.ErrCodeRoleNotFound, "role with id %s doesn't exist", id)
return store.sqlstore.WrapNotFoundErrf(err, roletypes.ErrCodeRoleNotFound, "role with id %s doesn't exist", id)
}
return nil

View File

@@ -8,6 +8,7 @@ import (
"github.com/SigNoz/signoz/pkg/authz/openfgaserver"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/factory"
@@ -18,7 +19,7 @@ import (
type provider struct {
server *openfgaserver.Server
store authtypes.RoleStore
store roletypes.Store
}
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile) factory.ProviderFactory[authz.AuthZ, authz.Config] {
@@ -67,61 +68,61 @@ func (provider *provider) ListObjects(ctx context.Context, subject string, relat
return provider.server.ListObjects(ctx, subject, relation, typeable)
}
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.Role, error) {
storableRole, err := provider.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
return authtypes.NewRoleFromStorableRole(storableRole), nil
return roletypes.NewRoleFromStorableRole(storableRole), nil
}
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.Role, error) {
storableRole, err := provider.store.GetByOrgIDAndName(ctx, orgID, name)
if err != nil {
return nil, err
}
return authtypes.NewRoleFromStorableRole(storableRole), nil
return roletypes.NewRoleFromStorableRole(storableRole), nil
}
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.Role, error) {
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.Role, error) {
storableRoles, err := provider.store.List(ctx, orgID)
if err != nil {
return nil, err
}
roles := make([]*authtypes.Role, len(storableRoles))
roles := make([]*roletypes.Role, len(storableRoles))
for idx, storableRole := range storableRoles {
roles[idx] = authtypes.NewRoleFromStorableRole(storableRole)
roles[idx] = roletypes.NewRoleFromStorableRole(storableRole)
}
return roles, nil
}
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.Role, error) {
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.Role, error) {
storableRoles, err := provider.store.ListByOrgIDAndNames(ctx, orgID, names)
if err != nil {
return nil, err
}
roles := make([]*authtypes.Role, len(storableRoles))
roles := make([]*roletypes.Role, len(storableRoles))
for idx, storable := range storableRoles {
roles[idx] = authtypes.NewRoleFromStorableRole(storable)
roles[idx] = roletypes.NewRoleFromStorableRole(storable)
}
return roles, nil
}
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.Role, error) {
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*roletypes.Role, error) {
storableRoles, err := provider.store.ListByOrgIDAndIDs(ctx, orgID, ids)
if err != nil {
return nil, err
}
roles := make([]*authtypes.Role, len(storableRoles))
roles := make([]*roletypes.Role, len(storableRoles))
for idx, storable := range storableRoles {
roles[idx] = authtypes.NewRoleFromStorableRole(storable)
roles[idx] = roletypes.NewRoleFromStorableRole(storable)
}
return roles, nil
@@ -178,10 +179,10 @@ func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, names [
return provider.Write(ctx, nil, tuples)
}
func (provider *provider) CreateManagedRoles(ctx context.Context, _ valuer.UUID, managedRoles []*authtypes.Role) error {
func (provider *provider) CreateManagedRoles(ctx context.Context, _ valuer.UUID, managedRoles []*roletypes.Role) error {
err := provider.store.RunInTx(ctx, func(ctx context.Context) error {
for _, role := range managedRoles {
err := provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
err := provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
if err != nil {
return err
}
@@ -198,15 +199,15 @@ func (provider *provider) CreateManagedRoles(ctx context.Context, _ valuer.UUID,
}
func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) error {
return provider.Grant(ctx, orgID, []string{authtypes.SigNozAdminRoleName}, authtypes.MustNewSubject(authtypes.TypeableUser, userID.String(), orgID, nil))
return provider.Grant(ctx, orgID, []string{roletypes.SigNozAdminRoleName}, authtypes.MustNewSubject(authtypes.TypeableUser, userID.String(), orgID, nil))
}
func (setter *provider) Create(_ context.Context, _ valuer.UUID, _ *authtypes.Role) error {
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
func (setter *provider) Create(_ context.Context, _ valuer.UUID, _ *roletypes.Role) error {
return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) GetOrCreate(_ context.Context, _ valuer.UUID, _ *authtypes.Role) (*authtypes.Role, error) {
return nil, errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
func (provider *provider) GetOrCreate(_ context.Context, _ valuer.UUID, _ *roletypes.Role) (*roletypes.Role, error) {
return nil, errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) GetResources(_ context.Context) []*authtypes.Resource {
@@ -214,19 +215,19 @@ func (provider *provider) GetResources(_ context.Context) []*authtypes.Resource
}
func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*authtypes.Object, error) {
return nil, errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
return nil, errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) Patch(_ context.Context, _ valuer.UUID, _ *authtypes.Role) error {
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
func (provider *provider) Patch(_ context.Context, _ valuer.UUID, _ *roletypes.Role) error {
return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) PatchObjects(_ context.Context, _ valuer.UUID, _ string, _ authtypes.Relation, _, _ []*authtypes.Object) error {
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) Delete(_ context.Context, _ valuer.UUID, _ valuer.UUID) error {
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) MustGetTypeables() []authtypes.Typeable {

View File

@@ -128,22 +128,9 @@ func (server *Server) BatchCheck(ctx context.Context, tupleReq map[string]*openf
}
func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, _ authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector, roleSelectors []authtypes.Selector) error {
subject := ""
switch claims.Principal {
case authtypes.PrincipalUser.StringValue():
user, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
if err != nil {
return err
}
subject = user
case authtypes.PrincipalServiceAccount.StringValue():
serviceAccount, err := authtypes.NewSubject(authtypes.TypeableServiceAccount, claims.ServiceAccountID, orgID, nil)
if err != nil {
return err
}
subject = serviceAccount
subject, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
if err != nil {
return err
}
tupleSlice, err := authtypes.TypeableRole.Tuples(subject, authtypes.RelationAssignee, roleSelectors, orgID)

View File

@@ -9,6 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -29,13 +30,13 @@ func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
return
}
req := new(authtypes.PostableRole)
req := new(roletypes.PostableRole)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
role := authtypes.NewRole(req.Name, req.Description, authtypes.RoleTypeCustom, valuer.MustNewUUID(claims.OrgID))
role := roletypes.NewRole(req.Name, req.Description, roletypes.RoleTypeCustom, valuer.MustNewUUID(claims.OrgID))
err = handler.authz.Create(ctx, valuer.MustNewUUID(claims.OrgID), role)
if err != nil {
render.Error(rw, err)
@@ -55,7 +56,7 @@ func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
id, ok := mux.Vars(r)["id"]
if !ok {
render.Error(rw, errors.New(errors.TypeInvalidInput, authtypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
return
}
roleID, err := valuer.NewUUID(id)
@@ -83,7 +84,7 @@ func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) {
id, ok := mux.Vars(r)["id"]
if !ok {
render.Error(rw, errors.New(errors.TypeInvalidInput, authtypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
return
}
roleID, err := valuer.NewUUID(id)
@@ -94,7 +95,7 @@ func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) {
relationStr, ok := mux.Vars(r)["relation"]
if !ok {
render.Error(rw, errors.New(errors.TypeInvalidInput, authtypes.ErrCodeRoleInvalidInput, "relation is missing from the request"))
render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "relation is missing from the request"))
return
}
relation, err := authtypes.NewRelation(relationStr)
@@ -149,7 +150,7 @@ func (handler *handler) Patch(rw http.ResponseWriter, r *http.Request) {
return
}
req := new(authtypes.PatchableRole)
req := new(roletypes.PatchableRole)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return

View File

@@ -0,0 +1,143 @@
package middleware
import (
"context"
"log/slog"
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/sharder"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"golang.org/x/sync/singleflight"
)
const (
apiKeyCrossOrgMessage string = "::API-KEY-CROSS-ORG::"
)
type APIKey struct {
store sqlstore.SQLStore
uuid *authtypes.UUID
headers []string
logger *slog.Logger
sharder sharder.Sharder
sfGroup *singleflight.Group
}
func NewAPIKey(store sqlstore.SQLStore, headers []string, logger *slog.Logger, sharder sharder.Sharder) *APIKey {
return &APIKey{
store: store,
uuid: authtypes.NewUUID(),
headers: headers,
logger: logger,
sharder: sharder,
sfGroup: &singleflight.Group{},
}
}
func (a *APIKey) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var values []string
var apiKeyToken string
var apiKey types.StorableAPIKey
for _, header := range a.headers {
values = append(values, r.Header.Get(header))
}
ctx, err := a.uuid.ContextFromRequest(r.Context(), values...)
if err != nil {
next.ServeHTTP(w, r)
return
}
apiKeyToken, ok := authtypes.UUIDFromContext(ctx)
if !ok {
next.ServeHTTP(w, r)
return
}
err = a.
store.
BunDB().
NewSelect().
Model(&apiKey).
Where("token = ?", apiKeyToken).
Scan(r.Context())
if err != nil {
next.ServeHTTP(w, r)
return
}
// allow the APIKey if expires_at is not set
if apiKey.ExpiresAt.Before(time.Now()) && !apiKey.ExpiresAt.Equal(types.NEVER_EXPIRES) {
next.ServeHTTP(w, r)
return
}
// get user from db
user := types.User{}
err = a.store.BunDB().NewSelect().Model(&user).Where("id = ?", apiKey.UserID).Scan(r.Context())
if err != nil {
next.ServeHTTP(w, r)
return
}
jwt := authtypes.Claims{
UserID: user.ID.String(),
Role: apiKey.Role,
Email: user.Email.String(),
OrgID: user.OrgID.String(),
}
ctx = authtypes.NewContextWithClaims(ctx, jwt)
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
next.ServeHTTP(w, r)
return
}
if err := a.sharder.IsMyOwnedKey(r.Context(), types.NewOrganizationKey(valuer.MustNewUUID(claims.OrgID))); err != nil {
a.logger.ErrorContext(r.Context(), apiKeyCrossOrgMessage, "claims", claims, "error", err)
next.ServeHTTP(w, r)
return
}
ctx = ctxtypes.SetAuthType(ctx, ctxtypes.AuthTypeAPIKey)
comment := ctxtypes.CommentFromContext(ctx)
comment.Set("auth_type", ctxtypes.AuthTypeAPIKey.StringValue())
comment.Set("user_id", claims.UserID)
comment.Set("org_id", claims.OrgID)
r = r.WithContext(ctxtypes.NewContextWithComment(ctx, comment))
next.ServeHTTP(w, r)
lastUsedCtx := context.WithoutCancel(r.Context())
_, _, _ = a.sfGroup.Do(apiKey.ID.StringValue(), func() (any, error) {
apiKey.LastUsed = time.Now()
_, err = a.
store.
BunDB().
NewUpdate().
Model(&apiKey).
Column("last_used").
Where("token = ?", apiKeyToken).
Where("revoked = false").
Exec(lastUsedCtx)
if err != nil {
a.logger.ErrorContext(lastUsedCtx, "failed to update last used of api key", "error", err)
}
return true, nil
})
})
}

View File

@@ -22,50 +22,31 @@ const (
)
type AuthN struct {
tokenizer tokenizer.Tokenizer
serviceAccountTokenizer tokenizer.Tokenizer
headers []string
serviceAccountHeaders []string
sharder sharder.Sharder
logger *slog.Logger
sfGroup *singleflight.Group
tokenizer tokenizer.Tokenizer
headers []string
sharder sharder.Sharder
logger *slog.Logger
sfGroup *singleflight.Group
}
func NewAuthN(
headers []string,
serviceAccountHeaders []string,
sharder sharder.Sharder,
tokenizer tokenizer.Tokenizer,
serviceAccountTokenizer tokenizer.Tokenizer,
logger *slog.Logger,
) *AuthN {
func NewAuthN(headers []string, sharder sharder.Sharder, tokenizer tokenizer.Tokenizer, logger *slog.Logger) *AuthN {
return &AuthN{
headers: headers,
serviceAccountHeaders: serviceAccountHeaders,
sharder: sharder,
tokenizer: tokenizer,
serviceAccountTokenizer: serviceAccountTokenizer,
logger: logger,
sfGroup: &singleflight.Group{},
headers: headers,
sharder: sharder,
tokenizer: tokenizer,
logger: logger,
sfGroup: &singleflight.Group{},
}
}
func (a *AuthN) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var userHeaderValues []string
var values []string
for _, header := range a.headers {
userHeaderValues = append(userHeaderValues, r.Header.Get(header))
}
ctx, authType, activeTokenizer, err := a.authenticateUser(r.Context(), userHeaderValues...)
if err != nil {
var saHeaderValues []string
for _, header := range a.serviceAccountHeaders {
saHeaderValues = append(saHeaderValues, r.Header.Get(header))
}
ctx, authType, activeTokenizer, err = a.authenticateServiceAccount(ctx, saHeaderValues...)
values = append(values, r.Header.Get(header))
}
ctx, err := a.contextFromRequest(r.Context(), values...)
if err != nil {
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
@@ -86,34 +67,27 @@ func (a *AuthN) Wrap(next http.Handler) http.Handler {
return
}
ctx = ctxtypes.SetAuthType(ctx, authType)
ctx = ctxtypes.SetAuthType(ctx, ctxtypes.AuthTypeTokenizer)
comment := ctxtypes.CommentFromContext(ctx)
comment.Set("auth_type", authType.StringValue())
comment.Set("tokenizer_provider", activeTokenizer.Config().Provider)
comment.Set("auth_type", ctxtypes.AuthTypeTokenizer.StringValue())
comment.Set("tokenizer_provider", a.tokenizer.Config().Provider)
comment.Set("user_id", claims.UserID)
comment.Set("service_account_id", claims.ServiceAccountID)
comment.Set("principal", claims.Principal)
comment.Set("org_id", claims.OrgID)
r = r.WithContext(ctxtypes.NewContextWithComment(ctx, comment))
next.ServeHTTP(w, r)
// Track last observed at for the active tokenizer.
var token string
if authType == ctxtypes.AuthTypeAPIKey {
token, err = authtypes.ServiceAccountAPIKeyFromContext(r.Context())
} else {
token, err = authtypes.AccessTokenFromContext(r.Context())
}
accessToken, err := authtypes.AccessTokenFromContext(r.Context())
if err != nil {
next.ServeHTTP(w, r)
return
}
lastObservedAtCtx := context.WithoutCancel(r.Context())
_, _, _ = a.sfGroup.Do(token, func() (any, error) {
if err := activeTokenizer.SetLastObservedAt(lastObservedAtCtx, token, time.Now()); err != nil {
_, _, _ = a.sfGroup.Do(accessToken, func() (any, error) {
if err := a.tokenizer.SetLastObservedAt(lastObservedAtCtx, accessToken, time.Now()); err != nil {
a.logger.ErrorContext(lastObservedAtCtx, "failed to set last observed at", "error", err)
return false, err
}
@@ -123,60 +97,23 @@ func (a *AuthN) Wrap(next http.Handler) http.Handler {
})
}
func (a *AuthN) authenticateUser(ctx context.Context, values ...string) (context.Context, ctxtypes.AuthType, tokenizer.Tokenizer, error) {
func (a *AuthN) contextFromRequest(ctx context.Context, values ...string) (context.Context, error) {
ctx, err := a.contextFromAccessToken(ctx, values...)
if err != nil {
return ctx, ctxtypes.AuthTypeTokenizer, a.tokenizer, err
return ctx, err
}
accessToken, err := authtypes.AccessTokenFromContext(ctx)
if err != nil {
return ctx, ctxtypes.AuthTypeTokenizer, a.tokenizer, err
return ctx, err
}
identity, err := a.tokenizer.GetIdentity(ctx, accessToken)
authenticatedUser, err := a.tokenizer.GetIdentity(ctx, accessToken)
if err != nil {
return ctx, ctxtypes.AuthTypeTokenizer, a.tokenizer, err
return ctx, err
}
ctx = authtypes.NewContextWithClaims(ctx, identity.ToClaims())
return ctx, ctxtypes.AuthTypeTokenizer, a.tokenizer, nil
}
func (a *AuthN) authenticateServiceAccount(ctx context.Context, values ...string) (context.Context, ctxtypes.AuthType, tokenizer.Tokenizer, error) {
ctx, err := a.contextFromServiceAccountAPIKey(ctx, values...)
if err != nil {
return ctx, ctxtypes.AuthTypeAPIKey, a.serviceAccountTokenizer, err
}
apiKey, err := authtypes.ServiceAccountAPIKeyFromContext(ctx)
if err != nil {
return ctx, ctxtypes.AuthTypeAPIKey, a.serviceAccountTokenizer, err
}
identity, err := a.serviceAccountTokenizer.GetIdentity(ctx, apiKey)
if err != nil {
return ctx, ctxtypes.AuthTypeAPIKey, a.serviceAccountTokenizer, err
}
ctx = authtypes.NewContextWithClaims(ctx, identity.ToClaims())
return ctx, ctxtypes.AuthTypeAPIKey, a.serviceAccountTokenizer, nil
}
func (a *AuthN) contextFromServiceAccountAPIKey(ctx context.Context, values ...string) (context.Context, error) {
var value string
for _, v := range values {
if v != "" {
value = v
break
}
}
if value == "" {
return ctx, errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "missing api key header")
}
return authtypes.NewContextWithServiceAccountAPIKey(ctx, value), nil
return authtypes.NewContextWithClaims(ctx, authenticatedUser.ToClaims()), nil
}
func (a *AuthN) contextFromAccessToken(ctx context.Context, values ...string) (context.Context, error) {

View File

@@ -9,6 +9,8 @@ import (
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -40,10 +42,23 @@ func (middleware *AuthZ) ViewAccess(next http.HandlerFunc) http.HandlerFunc {
return
}
commentCtx := ctxtypes.CommentFromContext(ctx)
authtype, ok := commentCtx.Map()["auth_type"]
if ok && authtype == ctxtypes.AuthTypeAPIKey.StringValue() {
if err := claims.IsViewer(); err != nil {
middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims)
render.Error(rw, err)
return
}
next(rw, req)
return
}
selectors := []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozEditorRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozViewerRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozEditorRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozViewerRoleName),
}
err = middleware.authzService.CheckWithTupleCreation(
@@ -79,9 +94,22 @@ func (middleware *AuthZ) EditAccess(next http.HandlerFunc) http.HandlerFunc {
return
}
commentCtx := ctxtypes.CommentFromContext(ctx)
authtype, ok := commentCtx.Map()["auth_type"]
if ok && authtype == ctxtypes.AuthTypeAPIKey.StringValue() {
if err := claims.IsEditor(); err != nil {
middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims)
render.Error(rw, err)
return
}
next(rw, req)
return
}
selectors := []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozEditorRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozEditorRoleName),
}
err = middleware.authzService.CheckWithTupleCreation(
@@ -117,8 +145,21 @@ func (middleware *AuthZ) AdminAccess(next http.HandlerFunc) http.HandlerFunc {
return
}
commentCtx := ctxtypes.CommentFromContext(ctx)
authtype, ok := commentCtx.Map()["auth_type"]
if ok && authtype == ctxtypes.AuthTypeAPIKey.StringValue() {
if err := claims.IsAdmin(); err != nil {
middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims)
render.Error(rw, err)
return
}
next(rw, req)
return
}
selectors := []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName),
}
err = middleware.authzService.CheckWithTupleCreation(
@@ -147,33 +188,17 @@ func (middleware *AuthZ) AdminAccess(next http.HandlerFunc) http.HandlerFunc {
func (middleware *AuthZ) SelfAccess(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
claims, err := authtypes.ClaimsFromContext(req.Context())
if err != nil {
render.Error(rw, err)
return
}
selectors := []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
}
err = middleware.authzService.CheckWithTupleCreation(
ctx,
claims,
valuer.MustNewUUID(claims.OrgID),
authtypes.RelationAssignee,
authtypes.TypeableRole,
selectors,
selectors,
)
if err != nil {
id := mux.Vars(req)["id"]
if err := claims.IsSelfAccess(id); err != nil {
middleware.logger.WarnContext(req.Context(), authzDeniedMessage, "claims", claims)
render.Error(rw, err)
return
}
id := mux.Vars(req)["id"]
if err := claims.IsSelfAccess(id); err != nil {
middleware.logger.WarnContext(req.Context(), authzDeniedMessage, "claims", claims)
render.Error(rw, err)
return
}
next(rw, req)

View File

@@ -43,7 +43,7 @@ type Module interface {
Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, data dashboardtypes.UpdatableDashboard, diff int) (*dashboardtypes.Dashboard, error)
LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, lock bool) error
LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, role types.Role, lock bool) error
Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error

Some files were not shown because too many files have changed in this diff Show More