mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-01 18:10:21 +01:00
Compare commits
1 Commits
main
...
release/v0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fea2db1665 |
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -51,7 +51,6 @@ jobs:
|
||||
- alerts
|
||||
- ingestionkeys
|
||||
- rootuser
|
||||
- serviceaccount
|
||||
sqlstore-provider:
|
||||
- postgres
|
||||
- sqlite
|
||||
|
||||
@@ -354,13 +354,3 @@ identn:
|
||||
impersonation:
|
||||
# toggle impersonation identN, when enabled, all requests will impersonate the root user
|
||||
enabled: false
|
||||
|
||||
##################### Service Account #####################
|
||||
serviceaccount:
|
||||
email:
|
||||
# email domain for the service account principal
|
||||
domain: signozserviceaccount.com
|
||||
|
||||
analytics:
|
||||
# toggle service account analytics
|
||||
enabled: true
|
||||
|
||||
@@ -190,7 +190,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.117.1
|
||||
image: signoz/signoz:v0.118.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.117.1
|
||||
image: signoz/signoz:v0.118.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
volumes:
|
||||
|
||||
@@ -181,7 +181,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.117.1}
|
||||
image: signoz/signoz:${VERSION:-v0.118.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -109,7 +109,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.117.1}
|
||||
image: signoz/signoz:${VERSION:-v0.118.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -2447,7 +2447,7 @@ components:
|
||||
- start
|
||||
- end
|
||||
type: object
|
||||
ServiceaccounttypesGettableFactorAPIKey:
|
||||
ServiceaccounttypesFactorAPIKey:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
@@ -2457,6 +2457,8 @@ components:
|
||||
type: integer
|
||||
id:
|
||||
type: string
|
||||
key:
|
||||
type: string
|
||||
lastObservedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
@@ -2469,6 +2471,7 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- key
|
||||
- expiresAt
|
||||
- lastObservedAt
|
||||
- serviceAccountId
|
||||
@@ -2496,68 +2499,25 @@ components:
|
||||
type: object
|
||||
ServiceaccounttypesPostableServiceAccount:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
roles:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
ServiceaccounttypesPostableServiceAccountRole:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- email
|
||||
- roles
|
||||
type: object
|
||||
ServiceaccounttypesServiceAccount:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
orgId:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- email
|
||||
- status
|
||||
- orgId
|
||||
type: object
|
||||
ServiceaccounttypesServiceAccountRole:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
role:
|
||||
$ref: '#/components/schemas/AuthtypesRole'
|
||||
roleId:
|
||||
type: string
|
||||
serviceAccountId:
|
||||
type: string
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- serviceAccountId
|
||||
- roleId
|
||||
- role
|
||||
type: object
|
||||
ServiceaccounttypesServiceAccountWithRoles:
|
||||
properties:
|
||||
createdAt:
|
||||
deletedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
email:
|
||||
@@ -2568,10 +2528,9 @@ components:
|
||||
type: string
|
||||
orgId:
|
||||
type: string
|
||||
serviceAccountRoles:
|
||||
roles:
|
||||
items:
|
||||
$ref: '#/components/schemas/ServiceaccounttypesServiceAccountRole'
|
||||
nullable: true
|
||||
type: string
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
@@ -2582,9 +2541,10 @@ components:
|
||||
- id
|
||||
- name
|
||||
- email
|
||||
- roles
|
||||
- status
|
||||
- orgId
|
||||
- serviceAccountRoles
|
||||
- deletedAt
|
||||
type: object
|
||||
ServiceaccounttypesUpdatableFactorAPIKey:
|
||||
properties:
|
||||
@@ -2597,6 +2557,28 @@ components:
|
||||
- name
|
||||
- expiresAt
|
||||
type: object
|
||||
ServiceaccounttypesUpdatableServiceAccount:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
roles:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- name
|
||||
- email
|
||||
- roles
|
||||
type: object
|
||||
ServiceaccounttypesUpdatableServiceAccountStatus:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
TelemetrytypesFieldContext:
|
||||
enum:
|
||||
- metric
|
||||
@@ -2720,6 +2702,43 @@ components:
|
||||
required:
|
||||
- id
|
||||
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
|
||||
TypesIdentifiable:
|
||||
properties:
|
||||
id:
|
||||
@@ -2774,6 +2793,16 @@ components:
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
TypesPostableAPIKey:
|
||||
properties:
|
||||
expiresInDays:
|
||||
format: int64
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
role:
|
||||
type: string
|
||||
type: object
|
||||
TypesPostableBulkInviteRequest:
|
||||
properties:
|
||||
invites:
|
||||
@@ -2834,6 +2863,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
|
||||
TypesUpdatableUser:
|
||||
properties:
|
||||
displayName:
|
||||
@@ -4910,6 +4966,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
|
||||
@@ -5684,7 +5956,7 @@ paths:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/ServiceaccounttypesServiceAccountWithRoles'
|
||||
$ref: '#/components/schemas/ServiceaccounttypesServiceAccount'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
@@ -5738,7 +6010,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ServiceaccounttypesPostableServiceAccount'
|
||||
$ref: '#/components/schemas/ServiceaccounttypesUpdatableServiceAccount'
|
||||
responses:
|
||||
"204":
|
||||
content:
|
||||
@@ -5803,7 +6075,7 @@ paths:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/components/schemas/ServiceaccounttypesGettableFactorAPIKey'
|
||||
$ref: '#/components/schemas/ServiceaccounttypesFactorAPIKey'
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
@@ -6026,71 +6298,11 @@ paths:
|
||||
summary: Updates a service account key
|
||||
tags:
|
||||
- serviceaccount
|
||||
/api/v1/service_accounts/{id}/roles:
|
||||
get:
|
||||
/api/v1/service_accounts/{id}/status:
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint gets all the roles for the existing service account
|
||||
operationId: GetServiceAccountRoles
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/components/schemas/AuthtypesRole'
|
||||
nullable: true
|
||||
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
|
||||
"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: Gets service account roles
|
||||
tags:
|
||||
- serviceaccount
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint assigns a role to a service account
|
||||
operationId: CreateServiceAccountRole
|
||||
description: This endpoint updates an existing service account status
|
||||
operationId: UpdateServiceAccountStatus
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@@ -6101,22 +6313,14 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ServiceaccounttypesPostableServiceAccountRole'
|
||||
$ref: '#/components/schemas/ServiceaccounttypesUpdatableServiceAccountStatus'
|
||||
responses:
|
||||
"201":
|
||||
"204":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/TypesIdentifiable'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: Created
|
||||
type: string
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
@@ -6135,89 +6339,6 @@ paths:
|
||||
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: Create service account role
|
||||
tags:
|
||||
- serviceaccount
|
||||
/api/v1/service_accounts/{id}/roles/{rid}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: This endpoint revokes a role from service account
|
||||
operationId: DeleteServiceAccountRole
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: rid
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
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
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Delete service account role
|
||||
tags:
|
||||
- serviceaccount
|
||||
/api/v1/service_accounts/me:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint gets my service account
|
||||
operationId: GetMyServiceAccount
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/ServiceaccounttypesServiceAccountWithRoles'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
@@ -6230,38 +6351,12 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
summary: Gets my service account
|
||||
tags:
|
||||
- serviceaccount
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint gets my service account
|
||||
operationId: UpdateMyServiceAccount
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ServiceaccounttypesPostableServiceAccount'
|
||||
responses:
|
||||
"204":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: No Content
|
||||
"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
|
||||
summary: Updates my service account
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Updates a service account status
|
||||
tags:
|
||||
- serviceaccount
|
||||
/api/v1/user:
|
||||
|
||||
@@ -76,12 +76,12 @@ func (provider *provider) Start(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (provider *provider) Audit(ctx context.Context, event audittypes.AuditEvent) {
|
||||
if event.PrincipalAttributes.PrincipalOrgID.IsZero() {
|
||||
if event.PrincipalOrgID.IsZero() {
|
||||
provider.settings.Logger().WarnContext(ctx, "audit event dropped as org_id is zero")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := provider.licensing.GetActive(ctx, event.PrincipalAttributes.PrincipalOrgID); err != nil {
|
||||
if _, err := provider.licensing.GetActive(ctx, event.PrincipalOrgID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -34,22 +34,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:
|
||||
user, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subject = user
|
||||
case authtypes.PrincipalServiceAccount:
|
||||
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)
|
||||
|
||||
@@ -213,8 +213,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, isAdmin bool, lock bool) error {
|
||||
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, isAdmin, 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 {
|
||||
|
||||
@@ -14,9 +14,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"
|
||||
)
|
||||
@@ -49,7 +50,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
|
||||
return
|
||||
}
|
||||
|
||||
apiKey, apiErr := ah.getOrCreateCloudIntegrationFactorAPIKey(r.Context(), valuer.MustNewUUID(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:",
|
||||
@@ -109,44 +110,84 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
|
||||
ah.Respond(w, result)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getOrCreateCloudIntegrationFactorAPIKey(ctx context.Context, orgID valuer.UUID, cloudProvider string) (
|
||||
func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId string, cloudProvider string) (
|
||||
string, *basemodel.ApiError,
|
||||
) {
|
||||
integrationPATName := fmt.Sprintf("%s", cloudProvider)
|
||||
serviceAccount, apiErr := ah.getOrCreateCloudIntegrationServiceAccount(ctx, orgID)
|
||||
integrationPATName := fmt.Sprintf("%s integration", cloudProvider)
|
||||
|
||||
integrationUser, apiErr := ah.getOrCreateCloudIntegrationUser(ctx, orgId, cloudProvider)
|
||||
if apiErr != nil {
|
||||
return "", apiErr
|
||||
}
|
||||
|
||||
factorAPIKey, err := serviceAccount.NewFactorAPIKey(integrationPATName, 0)
|
||||
orgIdUUID, err := valuer.NewUUID(orgId)
|
||||
if err != nil {
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't parse orgId: %w", err,
|
||||
))
|
||||
}
|
||||
|
||||
allPats, err := ah.Signoz.Modules.UserSetter.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
|
||||
}
|
||||
}
|
||||
|
||||
slog.InfoContext(ctx, "no PAT found for cloud integration, creating a new one",
|
||||
"cloud_provider", cloudProvider,
|
||||
)
|
||||
|
||||
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,
|
||||
))
|
||||
}
|
||||
|
||||
factorAPIKey, err = ah.Signoz.Modules.ServiceAccount.GetOrCreateFactorAPIKey(ctx, factorAPIKey)
|
||||
err = ah.Signoz.Modules.UserSetter.CreateAPIKey(ctx, newPAT)
|
||||
if err != nil {
|
||||
return "", basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't create cloud integration PAT: %w", err,
|
||||
))
|
||||
}
|
||||
return factorAPIKey.Key, nil
|
||||
return newPAT.Token, nil
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getOrCreateCloudIntegrationServiceAccount(ctx context.Context, orgId valuer.UUID) (*serviceaccounttypes.ServiceAccount, *basemodel.ApiError) {
|
||||
domain := ah.Signoz.Modules.ServiceAccount.Config().Email.Domain
|
||||
cloudIntegrationServiceAccount := serviceaccounttypes.NewServiceAccount("integration", domain, serviceaccounttypes.ServiceAccountStatusActive, orgId)
|
||||
cloudIntegrationServiceAccount, err := ah.Signoz.Modules.ServiceAccount.GetOrCreate(ctx, orgId, cloudIntegrationServiceAccount)
|
||||
func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||
ctx context.Context, orgId string, cloudProvider string,
|
||||
) (*types.User, *basemodel.ApiError) {
|
||||
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider)
|
||||
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName))
|
||||
|
||||
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, valuer.MustNewUUID(orgId), types.UserStatusActive)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration service account: %w", err))
|
||||
}
|
||||
err = ah.Signoz.Modules.ServiceAccount.SetRoleByName(ctx, orgId, cloudIntegrationServiceAccount.ID, authtypes.SigNozViewerRoleName)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration service account: %w", err))
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
|
||||
}
|
||||
|
||||
return cloudIntegrationServiceAccount, nil
|
||||
password := types.MustGenerateFactorPassword(cloudIntegrationUser.ID.StringValue())
|
||||
|
||||
cloudIntegrationUser, err = ah.Signoz.Modules.UserSetter.GetOrCreateUser(
|
||||
ctx,
|
||||
cloudIntegrationUser,
|
||||
user.WithFactorPassword(password),
|
||||
user.WithRoleNames([]string{authtypes.SigNozViewerRoleName}),
|
||||
)
|
||||
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) (
|
||||
|
||||
@@ -229,7 +229,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
s.config.APIServer.Timeout.Default,
|
||||
s.config.APIServer.Timeout.Max,
|
||||
).Wrap)
|
||||
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, nil).Wrap)
|
||||
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||
r.Use(middleware.NewComment().Wrap)
|
||||
|
||||
apiHandler.RegisterRoutes(r, am)
|
||||
|
||||
@@ -306,19 +306,11 @@ describe('PrivateRoute', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should redirect /settings/access-tokens to /settings/service-accounts', () => {
|
||||
it('should redirect /settings/access-tokens to /settings/api-keys', () => {
|
||||
renderPrivateRoute({ initialRoute: '/settings/access-tokens' });
|
||||
|
||||
expect(screen.getByTestId('location-display')).toHaveTextContent(
|
||||
'/settings/service-accounts',
|
||||
);
|
||||
});
|
||||
|
||||
it('should redirect /settings/api-keys to /settings/service-accounts', () => {
|
||||
renderPrivateRoute({ initialRoute: '/settings/api-keys' });
|
||||
|
||||
expect(screen.getByTestId('location-display')).toHaveTextContent(
|
||||
'/settings/service-accounts',
|
||||
'/settings/api-keys',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -157,6 +157,10 @@ export const IngestionSettings = Loadable(
|
||||
() => import(/* webpackChunkName: "Ingestion Settings" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const APIKeys = Loadable(
|
||||
() => import(/* webpackChunkName: "All Settings" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const MySettings = Loadable(
|
||||
() => import(/* webpackChunkName: "All MySettings" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
@@ -513,7 +513,6 @@ export const oldRoutes = [
|
||||
'/logs-save-views',
|
||||
'/traces-save-views',
|
||||
'/settings/access-tokens',
|
||||
'/settings/api-keys',
|
||||
'/messaging-queues',
|
||||
'/alerts/edit',
|
||||
];
|
||||
@@ -524,8 +523,7 @@ export const oldNewRoutesMapping: Record<string, string> = {
|
||||
'/logs-explorer/live': '/logs/logs-explorer/live',
|
||||
'/logs-save-views': '/logs/saved-views',
|
||||
'/traces-save-views': '/traces/saved-views',
|
||||
'/settings/access-tokens': '/settings/service-accounts',
|
||||
'/settings/api-keys': '/settings/service-accounts',
|
||||
'/settings/access-tokens': '/settings/api-keys',
|
||||
'/messaging-queues': '/messaging-queues/overview',
|
||||
'/alerts/edit': '/alerts/overview',
|
||||
};
|
||||
|
||||
@@ -23,15 +23,9 @@ import type {
|
||||
CreateServiceAccount201,
|
||||
CreateServiceAccountKey201,
|
||||
CreateServiceAccountKeyPathParameters,
|
||||
CreateServiceAccountRole201,
|
||||
CreateServiceAccountRolePathParameters,
|
||||
DeleteServiceAccountPathParameters,
|
||||
DeleteServiceAccountRolePathParameters,
|
||||
GetMyServiceAccount200,
|
||||
GetServiceAccount200,
|
||||
GetServiceAccountPathParameters,
|
||||
GetServiceAccountRoles200,
|
||||
GetServiceAccountRolesPathParameters,
|
||||
ListServiceAccountKeys200,
|
||||
ListServiceAccountKeysPathParameters,
|
||||
ListServiceAccounts200,
|
||||
@@ -39,10 +33,12 @@ import type {
|
||||
RevokeServiceAccountKeyPathParameters,
|
||||
ServiceaccounttypesPostableFactorAPIKeyDTO,
|
||||
ServiceaccounttypesPostableServiceAccountDTO,
|
||||
ServiceaccounttypesPostableServiceAccountRoleDTO,
|
||||
ServiceaccounttypesUpdatableFactorAPIKeyDTO,
|
||||
ServiceaccounttypesUpdatableServiceAccountDTO,
|
||||
ServiceaccounttypesUpdatableServiceAccountStatusDTO,
|
||||
UpdateServiceAccountKeyPathParameters,
|
||||
UpdateServiceAccountPathParameters,
|
||||
UpdateServiceAccountStatusPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
/**
|
||||
@@ -403,13 +399,13 @@ export const invalidateGetServiceAccount = async (
|
||||
*/
|
||||
export const updateServiceAccount = (
|
||||
{ id }: UpdateServiceAccountPathParameters,
|
||||
serviceaccounttypesPostableServiceAccountDTO: BodyType<ServiceaccounttypesPostableServiceAccountDTO>,
|
||||
serviceaccounttypesUpdatableServiceAccountDTO: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v1/service_accounts/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: serviceaccounttypesPostableServiceAccountDTO,
|
||||
data: serviceaccounttypesUpdatableServiceAccountDTO,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -422,7 +418,7 @@ export const getUpdateServiceAccountMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateServiceAccountPathParameters;
|
||||
data: BodyType<ServiceaccounttypesPostableServiceAccountDTO>;
|
||||
data: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -431,7 +427,7 @@ export const getUpdateServiceAccountMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateServiceAccountPathParameters;
|
||||
data: BodyType<ServiceaccounttypesPostableServiceAccountDTO>;
|
||||
data: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
@@ -448,7 +444,7 @@ export const getUpdateServiceAccountMutationOptions = <
|
||||
Awaited<ReturnType<typeof updateServiceAccount>>,
|
||||
{
|
||||
pathParams: UpdateServiceAccountPathParameters;
|
||||
data: BodyType<ServiceaccounttypesPostableServiceAccountDTO>;
|
||||
data: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
@@ -462,7 +458,7 @@ export const getUpdateServiceAccountMutationOptions = <
|
||||
export type UpdateServiceAccountMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateServiceAccount>>
|
||||
>;
|
||||
export type UpdateServiceAccountMutationBody = BodyType<ServiceaccounttypesPostableServiceAccountDTO>;
|
||||
export type UpdateServiceAccountMutationBody = BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
|
||||
export type UpdateServiceAccountMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
@@ -477,7 +473,7 @@ export const useUpdateServiceAccount = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateServiceAccountPathParameters;
|
||||
data: BodyType<ServiceaccounttypesPostableServiceAccountDTO>;
|
||||
data: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -486,7 +482,7 @@ export const useUpdateServiceAccount = <
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateServiceAccountPathParameters;
|
||||
data: BodyType<ServiceaccounttypesPostableServiceAccountDTO>;
|
||||
data: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
@@ -875,150 +871,44 @@ export const useUpdateServiceAccountKey = <
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint gets all the roles for the existing service account
|
||||
* @summary Gets service account roles
|
||||
* This endpoint updates an existing service account status
|
||||
* @summary Updates a service account status
|
||||
*/
|
||||
export const getServiceAccountRoles = (
|
||||
{ id }: GetServiceAccountRolesPathParameters,
|
||||
signal?: AbortSignal,
|
||||
export const updateServiceAccountStatus = (
|
||||
{ id }: UpdateServiceAccountStatusPathParameters,
|
||||
serviceaccounttypesUpdatableServiceAccountStatusDTO: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetServiceAccountRoles200>({
|
||||
url: `/api/v1/service_accounts/${id}/roles`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetServiceAccountRolesQueryKey = ({
|
||||
id,
|
||||
}: GetServiceAccountRolesPathParameters) => {
|
||||
return [`/api/v1/service_accounts/${id}/roles`] as const;
|
||||
};
|
||||
|
||||
export const getGetServiceAccountRolesQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getServiceAccountRoles>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetServiceAccountRolesPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getServiceAccountRoles>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetServiceAccountRolesQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getServiceAccountRoles>>
|
||||
> = ({ signal }) => getServiceAccountRoles({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getServiceAccountRoles>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetServiceAccountRolesQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getServiceAccountRoles>>
|
||||
>;
|
||||
export type GetServiceAccountRolesQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Gets service account roles
|
||||
*/
|
||||
|
||||
export function useGetServiceAccountRoles<
|
||||
TData = Awaited<ReturnType<typeof getServiceAccountRoles>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetServiceAccountRolesPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getServiceAccountRoles>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetServiceAccountRolesQueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Gets service account roles
|
||||
*/
|
||||
export const invalidateGetServiceAccountRoles = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetServiceAccountRolesPathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetServiceAccountRolesQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint assigns a role to a service account
|
||||
* @summary Create service account role
|
||||
*/
|
||||
export const createServiceAccountRole = (
|
||||
{ id }: CreateServiceAccountRolePathParameters,
|
||||
serviceaccounttypesPostableServiceAccountRoleDTO: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateServiceAccountRole201>({
|
||||
url: `/api/v1/service_accounts/${id}/roles`,
|
||||
method: 'POST',
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v1/service_accounts/${id}/status`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: serviceaccounttypesPostableServiceAccountRoleDTO,
|
||||
signal,
|
||||
data: serviceaccounttypesUpdatableServiceAccountStatusDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateServiceAccountRoleMutationOptions = <
|
||||
export const getUpdateServiceAccountStatusMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>,
|
||||
Awaited<ReturnType<typeof updateServiceAccountStatus>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreateServiceAccountRolePathParameters;
|
||||
data: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
|
||||
pathParams: UpdateServiceAccountStatusPathParameters;
|
||||
data: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>,
|
||||
Awaited<ReturnType<typeof updateServiceAccountStatus>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreateServiceAccountRolePathParameters;
|
||||
data: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
|
||||
pathParams: UpdateServiceAccountStatusPathParameters;
|
||||
data: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createServiceAccountRole'];
|
||||
const mutationKey = ['updateServiceAccountStatus'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
@@ -1028,299 +918,52 @@ export const getCreateServiceAccountRoleMutationOptions = <
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>,
|
||||
Awaited<ReturnType<typeof updateServiceAccountStatus>>,
|
||||
{
|
||||
pathParams: CreateServiceAccountRolePathParameters;
|
||||
data: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
|
||||
pathParams: UpdateServiceAccountStatusPathParameters;
|
||||
data: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return createServiceAccountRole(pathParams, data);
|
||||
return updateServiceAccountStatus(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateServiceAccountRoleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>
|
||||
export type UpdateServiceAccountStatusMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateServiceAccountStatus>>
|
||||
>;
|
||||
export type CreateServiceAccountRoleMutationBody = BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
|
||||
export type CreateServiceAccountRoleMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
export type UpdateServiceAccountStatusMutationBody = BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
|
||||
export type UpdateServiceAccountStatusMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create service account role
|
||||
* @summary Updates a service account status
|
||||
*/
|
||||
export const useCreateServiceAccountRole = <
|
||||
export const useUpdateServiceAccountStatus = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>,
|
||||
Awaited<ReturnType<typeof updateServiceAccountStatus>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreateServiceAccountRolePathParameters;
|
||||
data: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
|
||||
pathParams: UpdateServiceAccountStatusPathParameters;
|
||||
data: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>,
|
||||
Awaited<ReturnType<typeof updateServiceAccountStatus>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreateServiceAccountRolePathParameters;
|
||||
data: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
|
||||
pathParams: UpdateServiceAccountStatusPathParameters;
|
||||
data: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getCreateServiceAccountRoleMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint revokes a role from service account
|
||||
* @summary Delete service account role
|
||||
*/
|
||||
export const deleteServiceAccountRole = ({
|
||||
id,
|
||||
rid,
|
||||
}: DeleteServiceAccountRolePathParameters) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v1/service_accounts/${id}/roles/${rid}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteServiceAccountRoleMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
|
||||
TError,
|
||||
{ pathParams: DeleteServiceAccountRolePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
|
||||
TError,
|
||||
{ pathParams: DeleteServiceAccountRolePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteServiceAccountRole'];
|
||||
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 deleteServiceAccountRole>>,
|
||||
{ pathParams: DeleteServiceAccountRolePathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteServiceAccountRole(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteServiceAccountRoleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>
|
||||
>;
|
||||
|
||||
export type DeleteServiceAccountRoleMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Delete service account role
|
||||
*/
|
||||
export const useDeleteServiceAccountRole = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
|
||||
TError,
|
||||
{ pathParams: DeleteServiceAccountRolePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
|
||||
TError,
|
||||
{ pathParams: DeleteServiceAccountRolePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getDeleteServiceAccountRoleMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint gets my service account
|
||||
* @summary Gets my service account
|
||||
*/
|
||||
export const getMyServiceAccount = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<GetMyServiceAccount200>({
|
||||
url: `/api/v1/service_accounts/me`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetMyServiceAccountQueryKey = () => {
|
||||
return [`/api/v1/service_accounts/me`] as const;
|
||||
};
|
||||
|
||||
export const getGetMyServiceAccountQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getMyServiceAccount>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMyServiceAccount>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetMyServiceAccountQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getMyServiceAccount>>
|
||||
> = ({ signal }) => getMyServiceAccount(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMyServiceAccount>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetMyServiceAccountQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getMyServiceAccount>>
|
||||
>;
|
||||
export type GetMyServiceAccountQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Gets my service account
|
||||
*/
|
||||
|
||||
export function useGetMyServiceAccount<
|
||||
TData = Awaited<ReturnType<typeof getMyServiceAccount>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMyServiceAccount>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetMyServiceAccountQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Gets my service account
|
||||
*/
|
||||
export const invalidateGetMyServiceAccount = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetMyServiceAccountQueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint gets my service account
|
||||
* @summary Updates my service account
|
||||
*/
|
||||
export const updateMyServiceAccount = (
|
||||
serviceaccounttypesPostableServiceAccountDTO: BodyType<ServiceaccounttypesPostableServiceAccountDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v1/service_accounts/me`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: serviceaccounttypesPostableServiceAccountDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateMyServiceAccountMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMyServiceAccount>>,
|
||||
TError,
|
||||
{ data: BodyType<ServiceaccounttypesPostableServiceAccountDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMyServiceAccount>>,
|
||||
TError,
|
||||
{ data: BodyType<ServiceaccounttypesPostableServiceAccountDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateMyServiceAccount'];
|
||||
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 updateMyServiceAccount>>,
|
||||
{ data: BodyType<ServiceaccounttypesPostableServiceAccountDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return updateMyServiceAccount(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateMyServiceAccountMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateMyServiceAccount>>
|
||||
>;
|
||||
export type UpdateMyServiceAccountMutationBody = BodyType<ServiceaccounttypesPostableServiceAccountDTO>;
|
||||
export type UpdateMyServiceAccountMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Updates my service account
|
||||
*/
|
||||
export const useUpdateMyServiceAccount = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMyServiceAccount>>,
|
||||
TError,
|
||||
{ data: BodyType<ServiceaccounttypesPostableServiceAccountDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateMyServiceAccount>>,
|
||||
TError,
|
||||
{ data: BodyType<ServiceaccounttypesPostableServiceAccountDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getUpdateMyServiceAccountMutationOptions(options);
|
||||
const mutationOptions = getUpdateServiceAccountStatusMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
|
||||
@@ -2843,7 +2843,7 @@ export interface RulestatehistorytypesGettableRuleStateWindowDTO {
|
||||
state: RulestatehistorytypesAlertStateDTO;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
|
||||
export interface ServiceaccounttypesFactorAPIKeyDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -2858,6 +2858,10 @@ export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -2905,14 +2909,15 @@ export interface ServiceaccounttypesPostableServiceAccountDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesPostableServiceAccountRoleDTO {
|
||||
email: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
name: string;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesServiceAccountDTO {
|
||||
@@ -2921,65 +2926,11 @@ export interface ServiceaccounttypesServiceAccountDTO {
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
email: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesServiceAccountRoleDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
role: AuthtypesRoleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
roleId: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
serviceAccountId: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesServiceAccountWithRolesDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
deletedAt: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -2998,9 +2949,8 @@ export interface ServiceaccounttypesServiceAccountWithRolesDTO {
|
||||
orgId: string;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
serviceAccountRoles: ServiceaccounttypesServiceAccountRoleDTO[] | null;
|
||||
roles: string[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -3024,6 +2974,28 @@ export interface ServiceaccounttypesUpdatableFactorAPIKeyDTO {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesUpdatableServiceAccountDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
email: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesUpdatableServiceAccountStatusDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
}
|
||||
|
||||
export enum TelemetrytypesFieldContextDTO {
|
||||
metric = 'metric',
|
||||
log = 'log',
|
||||
@@ -3167,6 +3139,63 @@ export interface TypesDeprecatedUserDTO {
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
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 TypesIdentifiableDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -3249,6 +3278,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 TypesPostableBulkInviteRequestDTO {
|
||||
/**
|
||||
* @type array
|
||||
@@ -3328,6 +3373,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 TypesUpdatableUserDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -3866,6 +3956,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;
|
||||
};
|
||||
@@ -3970,7 +4085,7 @@ export type GetServiceAccountPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetServiceAccount200 = {
|
||||
data: ServiceaccounttypesServiceAccountWithRolesDTO;
|
||||
data: ServiceaccounttypesServiceAccountDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -3987,7 +4102,7 @@ export type ListServiceAccountKeys200 = {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
data: ServiceaccounttypesGettableFactorAPIKeyDTO[];
|
||||
data: ServiceaccounttypesFactorAPIKeyDTO[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -4013,44 +4128,9 @@ export type UpdateServiceAccountKeyPathParameters = {
|
||||
id: string;
|
||||
fid: string;
|
||||
};
|
||||
export type GetServiceAccountRolesPathParameters = {
|
||||
export type UpdateServiceAccountStatusPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetServiceAccountRoles200 = {
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
data: AuthtypesRoleDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateServiceAccountRolePathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type CreateServiceAccountRole201 = {
|
||||
data: TypesIdentifiableDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteServiceAccountRolePathParameters = {
|
||||
id: string;
|
||||
rid: string;
|
||||
};
|
||||
export type GetMyServiceAccount200 = {
|
||||
data: ServiceaccounttypesServiceAccountWithRolesDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListUsersDeprecated200 = {
|
||||
/**
|
||||
* @type array
|
||||
|
||||
@@ -21,6 +21,7 @@ import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type {
|
||||
ChangePasswordPathParameters,
|
||||
CreateAPIKey201,
|
||||
CreateInvite201,
|
||||
DeleteUserPathParameters,
|
||||
GetMyUser200,
|
||||
@@ -35,19 +36,24 @@ import type {
|
||||
GetUserPathParameters,
|
||||
GetUsersByRoleID200,
|
||||
GetUsersByRoleIDPathParameters,
|
||||
ListAPIKeys200,
|
||||
ListUsers200,
|
||||
ListUsersDeprecated200,
|
||||
RemoveUserRoleByUserIDAndRoleIDPathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
RevokeAPIKeyPathParameters,
|
||||
SetRoleByUserIDPathParameters,
|
||||
TypesChangePasswordRequestDTO,
|
||||
TypesDeprecatedUserDTO,
|
||||
TypesPostableAPIKeyDTO,
|
||||
TypesPostableBulkInviteRequestDTO,
|
||||
TypesPostableForgotPasswordDTO,
|
||||
TypesPostableInviteDTO,
|
||||
TypesPostableResetPasswordDTO,
|
||||
TypesPostableRoleDTO,
|
||||
TypesStorableAPIKeyDTO,
|
||||
TypesUpdatableUserDTO,
|
||||
UpdateAPIKeyPathParameters,
|
||||
UpdateUserDeprecated200,
|
||||
UpdateUserDeprecatedPathParameters,
|
||||
UpdateUserPathParameters,
|
||||
@@ -422,6 +428,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
|
||||
|
||||
28
frontend/src/api/v1/pats/create.ts
Normal file
28
frontend/src/api/v1/pats/create.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import {
|
||||
APIKeyProps,
|
||||
CreateAPIKeyProps,
|
||||
CreatePayloadProps,
|
||||
} from 'types/api/pat/types';
|
||||
|
||||
const create = async (
|
||||
props: CreateAPIKeyProps,
|
||||
): Promise<SuccessResponseV2<APIKeyProps>> => {
|
||||
try {
|
||||
const response = await axios.post<CreatePayloadProps>('/pats', {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default create;
|
||||
19
frontend/src/api/v1/pats/delete.ts
Normal file
19
frontend/src/api/v1/pats/delete.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
|
||||
const deleteAPIKey = async (id: string): Promise<SuccessResponseV2<null>> => {
|
||||
try {
|
||||
const response = await axios.delete(`/pats/${id}`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: null,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default deleteAPIKey;
|
||||
20
frontend/src/api/v1/pats/list.ts
Normal file
20
frontend/src/api/v1/pats/list.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { AllAPIKeyProps, APIKeyProps } from 'types/api/pat/types';
|
||||
|
||||
const list = async (): Promise<SuccessResponseV2<APIKeyProps[]>> => {
|
||||
try {
|
||||
const response = await axios.get<AllAPIKeyProps>('/pats');
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default list;
|
||||
24
frontend/src/api/v1/pats/update.ts
Normal file
24
frontend/src/api/v1/pats/update.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { UpdateAPIKeyProps } from 'types/api/pat/types';
|
||||
|
||||
const updateAPIKey = async (
|
||||
props: UpdateAPIKeyProps,
|
||||
): Promise<SuccessResponseV2<null>> => {
|
||||
try {
|
||||
const response = await axios.put(`/pats/${props.id}`, {
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: null,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default updateAPIKey;
|
||||
@@ -12,13 +12,17 @@ import {
|
||||
} from 'api/generated/services/serviceaccount';
|
||||
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import RolesSelect, { useRoles } from 'components/RolesSelect';
|
||||
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs';
|
||||
import { EMAIL_REGEX } from 'utils/app';
|
||||
|
||||
import './CreateServiceAccountModal.styles.scss';
|
||||
|
||||
interface FormValues {
|
||||
name: string;
|
||||
email: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
function CreateServiceAccountModal(): JSX.Element {
|
||||
@@ -37,6 +41,8 @@ function CreateServiceAccountModal(): JSX.Element {
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
name: '',
|
||||
email: '',
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -64,6 +70,13 @@ function CreateServiceAccountModal(): JSX.Element {
|
||||
},
|
||||
},
|
||||
});
|
||||
const {
|
||||
roles,
|
||||
isLoading: rolesLoading,
|
||||
isError: rolesError,
|
||||
error: rolesErrorObj,
|
||||
refetch: refetchRoles,
|
||||
} = useRoles();
|
||||
|
||||
function handleClose(): void {
|
||||
reset();
|
||||
@@ -74,6 +87,8 @@ function CreateServiceAccountModal(): JSX.Element {
|
||||
createServiceAccount({
|
||||
data: {
|
||||
name: values.name.trim(),
|
||||
email: values.email.trim(),
|
||||
roles: values.roles,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -119,6 +134,68 @@ function CreateServiceAccountModal(): JSX.Element {
|
||||
<p className="create-sa-form__error">{errors.name.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="create-sa-form__item">
|
||||
<label htmlFor="sa-email">Email Address</label>
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
rules={{
|
||||
required: 'Email Address is required',
|
||||
pattern: {
|
||||
value: EMAIL_REGEX,
|
||||
message: 'Please enter a valid email address',
|
||||
},
|
||||
}}
|
||||
render={({ field }): JSX.Element => (
|
||||
<Input
|
||||
id="sa-email"
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
className="create-sa-form__input"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="create-sa-form__error">{errors.email.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="create-sa-form__helper">
|
||||
Used only for notifications about this service account. It is not used for
|
||||
authentication.
|
||||
</p>
|
||||
|
||||
<div className="create-sa-form__item">
|
||||
<label htmlFor="sa-roles">Roles</label>
|
||||
<Controller
|
||||
name="roles"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (value): string | true =>
|
||||
value.length > 0 || 'At least one role is required',
|
||||
}}
|
||||
render={({ field }): JSX.Element => (
|
||||
<RolesSelect
|
||||
id="sa-roles"
|
||||
mode="multiple"
|
||||
roles={roles}
|
||||
loading={rolesLoading}
|
||||
isError={rolesError}
|
||||
error={rolesErrorObj}
|
||||
onRefetch={refetchRoles}
|
||||
placeholder="Select roles"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.roles && (
|
||||
<p className="create-sa-form__error">{errors.roles.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
@@ -11,6 +12,7 @@ jest.mock('@signozhq/sonner', () => ({
|
||||
|
||||
const mockToast = jest.mocked(toast);
|
||||
|
||||
const ROLES_ENDPOINT = '*/api/v1/roles';
|
||||
const SERVICE_ACCOUNTS_ENDPOINT = '*/api/v1/service_accounts';
|
||||
|
||||
function renderModal(): ReturnType<typeof render> {
|
||||
@@ -25,6 +27,9 @@ describe('CreateServiceAccountModal', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
server.use(
|
||||
rest.get(ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
|
||||
),
|
||||
rest.post(SERVICE_ACCOUNTS_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(201), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
@@ -43,11 +48,38 @@ describe('CreateServiceAccountModal', () => {
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('submit button remains disabled when email is invalid', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderModal();
|
||||
|
||||
await user.type(screen.getByPlaceholderText('Enter a name'), 'My Bot');
|
||||
await user.type(
|
||||
screen.getByPlaceholderText('email@example.com'),
|
||||
'not-an-email',
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('Select roles'));
|
||||
await user.click(await screen.findByTitle('signoz-admin'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole('button', { name: /Create Service Account/i }),
|
||||
).toBeDisabled(),
|
||||
);
|
||||
});
|
||||
|
||||
it('successful submit shows toast.success and closes modal', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderModal();
|
||||
|
||||
await user.type(screen.getByPlaceholderText('Enter a name'), 'Deploy Bot');
|
||||
await user.type(
|
||||
screen.getByPlaceholderText('email@example.com'),
|
||||
'deploy@acme.io',
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('Select roles'));
|
||||
await user.click(await screen.findByTitle('signoz-admin'));
|
||||
|
||||
const submitBtn = screen.getByRole('button', {
|
||||
name: /Create Service Account/i,
|
||||
@@ -84,6 +116,13 @@ describe('CreateServiceAccountModal', () => {
|
||||
renderModal();
|
||||
|
||||
await user.type(screen.getByPlaceholderText('Enter a name'), 'Dupe Bot');
|
||||
await user.type(
|
||||
screen.getByPlaceholderText('email@example.com'),
|
||||
'dupe@acme.io',
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('Select roles'));
|
||||
await user.click(await screen.findByTitle('signoz-admin'));
|
||||
|
||||
const submitBtn = screen.getByRole('button', {
|
||||
name: /Create Service Account/i,
|
||||
@@ -125,4 +164,16 @@ describe('CreateServiceAccountModal', () => {
|
||||
|
||||
await screen.findByText('Name is required');
|
||||
});
|
||||
|
||||
it('shows "Please enter a valid email address" for a malformed email', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderModal();
|
||||
|
||||
await user.type(
|
||||
screen.getByPlaceholderText('email@example.com'),
|
||||
'not-an-email',
|
||||
);
|
||||
|
||||
await screen.findByText('Please enter a valid email address');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ export function useRoles(): {
|
||||
export function getRoleOptions(roles: AuthtypesRoleDTO[]): RoleOption[] {
|
||||
return roles.map((role) => ({
|
||||
label: role.name ?? '',
|
||||
value: role.id ?? '',
|
||||
value: role.name ?? '',
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
|
||||
import { Trash2, X } from '@signozhq/icons';
|
||||
import { PowerOff, X } from '@signozhq/icons';
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
getGetServiceAccountQueryKey,
|
||||
invalidateListServiceAccounts,
|
||||
useDeleteServiceAccount,
|
||||
useUpdateServiceAccountStatus,
|
||||
} from 'api/generated/services/serviceaccount';
|
||||
import type {
|
||||
RenderErrorResponseDTO,
|
||||
@@ -17,14 +17,14 @@ import { AxiosError } from 'axios';
|
||||
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs';
|
||||
|
||||
function DeleteAccountModal(): JSX.Element {
|
||||
function DisableAccountModal(): JSX.Element {
|
||||
const queryClient = useQueryClient();
|
||||
const [accountId, setAccountId] = useQueryState(SA_QUERY_PARAMS.ACCOUNT);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useQueryState(
|
||||
SA_QUERY_PARAMS.DELETE_SA,
|
||||
const [isDisableOpen, setIsDisableOpen] = useQueryState(
|
||||
SA_QUERY_PARAMS.DISABLE_SA,
|
||||
parseAsBoolean.withDefault(false),
|
||||
);
|
||||
const open = !!isDeleteOpen && !!accountId;
|
||||
const open = !!isDisableOpen && !!accountId;
|
||||
|
||||
const cachedAccount = accountId
|
||||
? queryClient.getQueryData<{
|
||||
@@ -34,13 +34,13 @@ function DeleteAccountModal(): JSX.Element {
|
||||
const accountName = cachedAccount?.data?.name;
|
||||
|
||||
const {
|
||||
mutate: deleteAccount,
|
||||
isLoading: isDeleting,
|
||||
} = useDeleteServiceAccount({
|
||||
mutate: updateStatus,
|
||||
isLoading: isDisabling,
|
||||
} = useUpdateServiceAccountStatus({
|
||||
mutation: {
|
||||
onSuccess: async () => {
|
||||
toast.success('Service account deleted', { richColors: true });
|
||||
await setIsDeleteOpen(null);
|
||||
toast.success('Service account disabled', { richColors: true });
|
||||
await setIsDisableOpen(null);
|
||||
await setAccountId(null);
|
||||
await invalidateListServiceAccounts(queryClient);
|
||||
},
|
||||
@@ -48,7 +48,7 @@ function DeleteAccountModal(): JSX.Element {
|
||||
const errMessage =
|
||||
convertToApiError(
|
||||
error as AxiosError<RenderErrorResponseDTO, unknown> | null,
|
||||
)?.getErrorMessage() || 'Failed to delete service account';
|
||||
)?.getErrorMessage() || 'Failed to disable service account';
|
||||
toast.error(errMessage, { richColors: true });
|
||||
},
|
||||
},
|
||||
@@ -58,13 +58,14 @@ function DeleteAccountModal(): JSX.Element {
|
||||
if (!accountId) {
|
||||
return;
|
||||
}
|
||||
deleteAccount({
|
||||
updateStatus({
|
||||
pathParams: { id: accountId },
|
||||
data: { status: 'DISABLED' },
|
||||
});
|
||||
}
|
||||
|
||||
function handleCancel(): void {
|
||||
setIsDeleteOpen(null);
|
||||
setIsDisableOpen(null);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -75,18 +76,17 @@ function DeleteAccountModal(): JSX.Element {
|
||||
handleCancel();
|
||||
}
|
||||
}}
|
||||
title={`Delete service account ${accountName ?? ''}?`}
|
||||
title={`Disable service account ${accountName ?? ''}?`}
|
||||
width="narrow"
|
||||
className="alert-dialog sa-delete-dialog"
|
||||
className="alert-dialog sa-disable-dialog"
|
||||
showCloseButton={false}
|
||||
disableOutsideClick={false}
|
||||
>
|
||||
<p className="sa-delete-dialog__body">
|
||||
Are you sure you want to delete <strong>{accountName}</strong>? This action
|
||||
cannot be undone. All keys associated with this service account will be
|
||||
permanently removed.
|
||||
<p className="sa-disable-dialog__body">
|
||||
Disabling this service account will revoke access for all its keys. Any
|
||||
systems using this account will lose access immediately.
|
||||
</p>
|
||||
<DialogFooter className="sa-delete-dialog__footer">
|
||||
<DialogFooter className="sa-disable-dialog__footer">
|
||||
<Button variant="solid" color="secondary" size="sm" onClick={handleCancel}>
|
||||
<X size={12} />
|
||||
Cancel
|
||||
@@ -95,15 +95,15 @@ function DeleteAccountModal(): JSX.Element {
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
size="sm"
|
||||
loading={isDeleting}
|
||||
loading={isDisabling}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Delete
|
||||
<PowerOff size={12} />
|
||||
Disable
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeleteAccountModal;
|
||||
export default DisableAccountModal;
|
||||
@@ -6,7 +6,7 @@ import { LockKeyhole, Trash2, X } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
|
||||
import { DatePicker } from 'antd';
|
||||
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { ServiceaccounttypesFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { disabledDate, formatLastObservedAt } from '../utils';
|
||||
@@ -17,7 +17,7 @@ export interface EditKeyFormProps {
|
||||
register: UseFormRegister<FormValues>;
|
||||
control: Control<FormValues>;
|
||||
expiryMode: ExpiryMode;
|
||||
keyItem: ServiceaccounttypesGettableFactorAPIKeyDTO | null;
|
||||
keyItem: ServiceaccounttypesFactorAPIKeyDTO | null;
|
||||
isSaving: boolean;
|
||||
isDirty: boolean;
|
||||
onSubmit: () => void;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from 'api/generated/services/serviceaccount';
|
||||
import type {
|
||||
RenderErrorResponseDTO,
|
||||
ServiceaccounttypesGettableFactorAPIKeyDTO,
|
||||
ServiceaccounttypesFactorAPIKeyDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
|
||||
@@ -27,7 +27,7 @@ import { DEFAULT_FORM_VALUES, ExpiryMode } from './types';
|
||||
import './EditKeyModal.styles.scss';
|
||||
|
||||
export interface EditKeyModalProps {
|
||||
keyItem: ServiceaccounttypesGettableFactorAPIKeyDTO | null;
|
||||
keyItem: ServiceaccounttypesFactorAPIKeyDTO | null;
|
||||
}
|
||||
|
||||
function EditKeyModal({ keyItem }: EditKeyModalProps): JSX.Element {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button } from '@signozhq/button';
|
||||
import { KeyRound, X } from '@signozhq/icons';
|
||||
import { Skeleton, Table, Tooltip } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table/interface';
|
||||
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { ServiceaccounttypesFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import dayjs from 'dayjs';
|
||||
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs';
|
||||
@@ -14,7 +14,7 @@ import RevokeKeyModal from './RevokeKeyModal';
|
||||
import { formatLastObservedAt } from './utils';
|
||||
|
||||
interface KeysTabProps {
|
||||
keys: ServiceaccounttypesGettableFactorAPIKeyDTO[];
|
||||
keys: ServiceaccounttypesFactorAPIKeyDTO[];
|
||||
isLoading: boolean;
|
||||
isDisabled?: boolean;
|
||||
currentPage: number;
|
||||
@@ -44,7 +44,7 @@ function buildColumns({
|
||||
isDisabled,
|
||||
onRevokeClick,
|
||||
handleformatLastObservedAt,
|
||||
}: BuildColumnsParams): ColumnsType<ServiceaccounttypesGettableFactorAPIKeyDTO> {
|
||||
}: BuildColumnsParams): ColumnsType<ServiceaccounttypesFactorAPIKeyDTO> {
|
||||
return [
|
||||
{
|
||||
title: 'Name',
|
||||
@@ -183,7 +183,7 @@ function KeysTab({
|
||||
return (
|
||||
<>
|
||||
{/* Todo: use new table component from periscope when ready */}
|
||||
<Table<ServiceaccounttypesGettableFactorAPIKeyDTO>
|
||||
<Table<ServiceaccounttypesFactorAPIKeyDTO>
|
||||
columns={columns}
|
||||
dataSource={keys}
|
||||
rowKey="id"
|
||||
|
||||
@@ -9,9 +9,6 @@ import { ServiceAccountRow } from 'container/ServiceAccountsSettings/utils';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import SaveErrorItem from './SaveErrorItem';
|
||||
import type { SaveError } from './utils';
|
||||
|
||||
interface OverviewTabProps {
|
||||
account: ServiceAccountRow;
|
||||
localName: string;
|
||||
@@ -24,7 +21,6 @@ interface OverviewTabProps {
|
||||
rolesError?: boolean;
|
||||
rolesErrorObj?: APIError | undefined;
|
||||
onRefetchRoles?: () => void;
|
||||
saveErrors?: SaveError[];
|
||||
}
|
||||
|
||||
function OverviewTab({
|
||||
@@ -39,7 +35,6 @@ function OverviewTab({
|
||||
rolesError,
|
||||
rolesErrorObj,
|
||||
onRefetchRoles,
|
||||
saveErrors = [],
|
||||
}: OverviewTabProps): JSX.Element {
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
@@ -97,14 +92,11 @@ function OverviewTab({
|
||||
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
|
||||
<div className="sa-drawer__disabled-roles">
|
||||
{localRoles.length > 0 ? (
|
||||
localRoles.map((roleId) => {
|
||||
const role = availableRoles.find((r) => r.id === roleId);
|
||||
return (
|
||||
<Badge key={roleId} color="vanilla">
|
||||
{role?.name ?? roleId}
|
||||
</Badge>
|
||||
);
|
||||
})
|
||||
localRoles.map((r) => (
|
||||
<Badge key={r} color="vanilla">
|
||||
{r}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="sa-drawer__input-text">—</span>
|
||||
)}
|
||||
@@ -134,13 +126,9 @@ function OverviewTab({
|
||||
<Badge color="forest" variant="outline">
|
||||
ACTIVE
|
||||
</Badge>
|
||||
) : account.status?.toUpperCase() === 'DELETED' ? (
|
||||
<Badge color="cherry" variant="outline">
|
||||
DELETED
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge color="vanilla" variant="outline" className="sa-status-badge">
|
||||
{account.status ? account.status.toUpperCase() : 'UNKNOWN'}
|
||||
DISABLED
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -155,19 +143,6 @@ function OverviewTab({
|
||||
<Badge color="vanilla">{formatTimestamp(account.updatedAt)}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{saveErrors.length > 0 && (
|
||||
<div className="sa-drawer__save-errors">
|
||||
{saveErrors.map(({ context, apiError, onRetry }) => (
|
||||
<SaveErrorItem
|
||||
key={context}
|
||||
context={context}
|
||||
apiError={apiError}
|
||||
onRetry={onRetry}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from 'api/generated/services/serviceaccount';
|
||||
import type {
|
||||
RenderErrorResponseDTO,
|
||||
ServiceaccounttypesGettableFactorAPIKeyDTO,
|
||||
ServiceaccounttypesFactorAPIKeyDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
|
||||
@@ -64,9 +64,9 @@ function RevokeKeyModal(): JSX.Element {
|
||||
const open = !!revokeKeyId && !!accountId;
|
||||
|
||||
const cachedKeys = accountId
|
||||
? queryClient.getQueryData<{
|
||||
data: ServiceaccounttypesGettableFactorAPIKeyDTO[];
|
||||
}>(getListServiceAccountKeysQueryKey({ id: accountId }))
|
||||
? queryClient.getQueryData<{ data: ServiceaccounttypesFactorAPIKeyDTO[] }>(
|
||||
getListServiceAccountKeysQueryKey({ id: accountId }),
|
||||
)
|
||||
: null;
|
||||
const keyName = cachedKeys?.data?.find((k) => k.id === revokeKeyId)?.name;
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { ChevronDown, ChevronUp, CircleAlert, RotateCw } from '@signozhq/icons';
|
||||
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
interface SaveErrorItemProps {
|
||||
context: string;
|
||||
apiError: APIError;
|
||||
onRetry?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
function SaveErrorItem({
|
||||
context,
|
||||
apiError,
|
||||
onRetry,
|
||||
}: SaveErrorItemProps): JSX.Element {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [isRetrying, setIsRetrying] = useState(false);
|
||||
|
||||
const ChevronIcon = expanded ? ChevronUp : ChevronDown;
|
||||
|
||||
return (
|
||||
<div className="sa-error-item">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="sa-error-item__header"
|
||||
aria-disabled={isRetrying}
|
||||
onClick={(): void => {
|
||||
if (!isRetrying) {
|
||||
setExpanded((prev) => !prev);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CircleAlert size={12} className="sa-error-item__icon" />
|
||||
<span className="sa-error-item__title">
|
||||
{isRetrying ? 'Retrying...' : `${context}: ${apiError.getErrorMessage()}`}
|
||||
</span>
|
||||
{onRetry && !isRetrying && (
|
||||
<Button
|
||||
type="button"
|
||||
aria-label="Retry"
|
||||
size="xs"
|
||||
onClick={async (e): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
setIsRetrying(true);
|
||||
setExpanded(false);
|
||||
try {
|
||||
await onRetry();
|
||||
} finally {
|
||||
setIsRetrying(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RotateCw size={12} color={Color.BG_CHERRY_400} />
|
||||
</Button>
|
||||
)}
|
||||
{!isRetrying && (
|
||||
<ChevronIcon size={14} className="sa-error-item__chevron" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{expanded && !isRetrying && (
|
||||
<div className="sa-error-item__body">
|
||||
<ErrorContent error={apiError} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SaveErrorItem;
|
||||
@@ -92,23 +92,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.25rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(136, 136, 136, 0.4);
|
||||
border-radius: 0.125rem;
|
||||
|
||||
&:hover {
|
||||
background: rgba(136, 136, 136, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
@@ -256,113 +239,6 @@
|
||||
letter-spacing: 0.48px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&__save-errors {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
}
|
||||
|
||||
.sa-error-item {
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
width: 100%;
|
||||
padding: var(--padding-2) var(--padding-4);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
background: rgba(229, 72, 77, 0.08);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
&[aria-disabled='true'] {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--callout-error-border);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--bg-cherry-500);
|
||||
}
|
||||
|
||||
&__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--bg-cherry-500);
|
||||
line-height: var(--line-height-18);
|
||||
letter-spacing: -0.06px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__chevron {
|
||||
flex-shrink: 0;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
&__body {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
|
||||
.error-content {
|
||||
&__summary {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
&__summary-left {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__error-code {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
&__error-message {
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&__docs-button {
|
||||
font-size: 11px;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
&__message-badge {
|
||||
padding: 0 12px 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__message-item {
|
||||
font-size: 11px;
|
||||
padding: 2px 12px 2px 22px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.keys-tab {
|
||||
@@ -553,7 +429,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sa-delete-dialog {
|
||||
.sa-disable-dialog {
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { DrawerWrapper } from '@signozhq/drawer';
|
||||
import { Key, LayoutGrid, Plus, Trash2, X } from '@signozhq/icons';
|
||||
import { Key, LayoutGrid, Plus, PowerOff, X } from '@signozhq/icons';
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
|
||||
import { Pagination, Skeleton } from 'antd';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
getListServiceAccountsQueryKey,
|
||||
useGetServiceAccount,
|
||||
useListServiceAccountKeys,
|
||||
useUpdateServiceAccount,
|
||||
} from 'api/generated/services/serviceaccount';
|
||||
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import { useRoles } from 'components/RolesSelect';
|
||||
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
|
||||
import {
|
||||
ServiceAccountRow,
|
||||
ServiceAccountStatus,
|
||||
toServiceAccountRow,
|
||||
} from 'container/ServiceAccountsSettings/utils';
|
||||
import { useServiceAccountRoleManager } from 'hooks/serviceAccount/useServiceAccountRoleManager';
|
||||
import {
|
||||
parseAsBoolean,
|
||||
parseAsInteger,
|
||||
@@ -31,14 +27,12 @@ import {
|
||||
parseAsStringEnum,
|
||||
useQueryState,
|
||||
} from 'nuqs';
|
||||
import APIError from 'types/api/error';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import AddKeyModal from './AddKeyModal';
|
||||
import DeleteAccountModal from './DeleteAccountModal';
|
||||
import DisableAccountModal from './DisableAccountModal';
|
||||
import KeysTab from './KeysTab';
|
||||
import OverviewTab from './OverviewTab';
|
||||
import type { SaveError } from './utils';
|
||||
import { ServiceAccountDrawerTab } from './utils';
|
||||
|
||||
import './ServiceAccountDrawer.styles.scss';
|
||||
@@ -75,16 +69,12 @@ function ServiceAccountDrawer({
|
||||
SA_QUERY_PARAMS.ADD_KEY,
|
||||
parseAsBoolean.withDefault(false),
|
||||
);
|
||||
const [, setIsDeleteOpen] = useQueryState(
|
||||
SA_QUERY_PARAMS.DELETE_SA,
|
||||
const [, setIsDisableOpen] = useQueryState(
|
||||
SA_QUERY_PARAMS.DISABLE_SA,
|
||||
parseAsBoolean.withDefault(false),
|
||||
);
|
||||
const [localName, setLocalName] = useState('');
|
||||
const [localRoles, setLocalRoles] = useState<string[]>([]);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveErrors, setSaveErrors] = useState<SaveError[]>([]);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
data: accountData,
|
||||
@@ -103,30 +93,21 @@ function ServiceAccountDrawer({
|
||||
[accountData],
|
||||
);
|
||||
|
||||
const { currentRoles, applyDiff } = useServiceAccountRoleManager(
|
||||
selectedAccountId ?? '',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (account?.id) {
|
||||
setLocalName(account?.name ?? '');
|
||||
if (account) {
|
||||
setLocalName(account.name ?? '');
|
||||
setLocalRoles(account.roles ?? []);
|
||||
setKeysPage(1);
|
||||
}
|
||||
setSaveErrors([]);
|
||||
}, [account?.id, account?.name, setKeysPage]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [account?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalRoles(currentRoles.map((r) => r.id).filter(Boolean) as string[]);
|
||||
}, [currentRoles]);
|
||||
|
||||
const isDeleted =
|
||||
account?.status?.toUpperCase() === ServiceAccountStatus.Deleted;
|
||||
const isDisabled = account?.status?.toUpperCase() !== 'ACTIVE';
|
||||
|
||||
const isDirty =
|
||||
account !== null &&
|
||||
(localName !== (account.name ?? '') ||
|
||||
JSON.stringify([...localRoles].sort()) !==
|
||||
JSON.stringify([...currentRoles.map((r) => r.id).filter(Boolean)].sort()));
|
||||
JSON.stringify(localRoles) !== JSON.stringify(account.roles ?? []));
|
||||
|
||||
const {
|
||||
roles: availableRoles,
|
||||
@@ -152,189 +133,51 @@ function ServiceAccountDrawer({
|
||||
}
|
||||
}, [keysLoading, keys.length, keysPage, setKeysPage]);
|
||||
|
||||
// the retry for this mutation is safe due to the api being idempotent on backend
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateServiceAccount();
|
||||
|
||||
const toSaveApiError = useCallback(
|
||||
(err: unknown): APIError =>
|
||||
convertToApiError(err as AxiosError<RenderErrorResponseDTO>) ??
|
||||
toAPIError(err as AxiosError<RenderErrorResponseDTO>),
|
||||
[],
|
||||
);
|
||||
|
||||
const retryNameUpdate = useCallback(async (): Promise<void> => {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await updateMutateAsync({
|
||||
pathParams: { id: account.id },
|
||||
data: { name: localName },
|
||||
});
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Name update'));
|
||||
refetchAccount();
|
||||
queryClient.invalidateQueries(getListServiceAccountsQueryKey());
|
||||
} catch (err) {
|
||||
setSaveErrors((prev) =>
|
||||
prev.map((e) =>
|
||||
e.context === 'Name update' ? { ...e, apiError: toSaveApiError(err) } : e,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [
|
||||
account,
|
||||
localName,
|
||||
updateMutateAsync,
|
||||
refetchAccount,
|
||||
queryClient,
|
||||
toSaveApiError,
|
||||
]);
|
||||
|
||||
const handleNameChange = useCallback((name: string): void => {
|
||||
setLocalName(name);
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Name update'));
|
||||
}, []);
|
||||
|
||||
const makeRoleRetry = useCallback(
|
||||
(
|
||||
context: string,
|
||||
rawRetry: () => Promise<void>,
|
||||
) => async (): Promise<void> => {
|
||||
try {
|
||||
await rawRetry();
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== context));
|
||||
} catch (err) {
|
||||
setSaveErrors((prev) =>
|
||||
prev.map((e) =>
|
||||
e.context === context ? { ...e, apiError: toSaveApiError(err) } : e,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
[toSaveApiError],
|
||||
);
|
||||
|
||||
const retryRolesUpdate = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const failures = await applyDiff(localRoles, availableRoles);
|
||||
if (failures.length === 0) {
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Roles update'));
|
||||
} else {
|
||||
setSaveErrors((prev) => {
|
||||
const rest = prev.filter((e) => e.context !== 'Roles update');
|
||||
const roleErrors = failures.map((f) => {
|
||||
const ctx = `Role '${f.roleName}'`;
|
||||
return {
|
||||
context: ctx,
|
||||
apiError: toSaveApiError(f.error),
|
||||
onRetry: makeRoleRetry(ctx, f.onRetry),
|
||||
};
|
||||
const { mutate: updateAccount, isLoading: isSaving } = useUpdateServiceAccount(
|
||||
{
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
toast.success('Service account updated successfully', {
|
||||
richColors: true,
|
||||
});
|
||||
return [...rest, ...roleErrors];
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setSaveErrors((prev) =>
|
||||
prev.map((e) =>
|
||||
e.context === 'Roles update' ? { ...e, apiError: toSaveApiError(err) } : e,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [localRoles, availableRoles, applyDiff, toSaveApiError, makeRoleRetry]);
|
||||
refetchAccount();
|
||||
onSuccess({ closeDrawer: false });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errMessage =
|
||||
convertToApiError(
|
||||
error as AxiosError<RenderErrorResponseDTO, unknown> | null,
|
||||
)?.getErrorMessage() || 'Failed to update service account';
|
||||
toast.error(errMessage, { richColors: true });
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const handleSave = useCallback(async (): Promise<void> => {
|
||||
function handleSave(): void {
|
||||
if (!account || !isDirty) {
|
||||
return;
|
||||
}
|
||||
setSaveErrors([]);
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const namePromise =
|
||||
localName !== (account.name ?? '')
|
||||
? updateMutateAsync({
|
||||
pathParams: { id: account.id },
|
||||
data: { name: localName },
|
||||
})
|
||||
: Promise.resolve();
|
||||
|
||||
const [nameResult, rolesResult] = await Promise.allSettled([
|
||||
namePromise,
|
||||
applyDiff(localRoles, availableRoles),
|
||||
]);
|
||||
|
||||
const errors: SaveError[] = [];
|
||||
|
||||
if (nameResult.status === 'rejected') {
|
||||
errors.push({
|
||||
context: 'Name update',
|
||||
apiError: toSaveApiError(nameResult.reason),
|
||||
onRetry: retryNameUpdate,
|
||||
});
|
||||
}
|
||||
|
||||
if (rolesResult.status === 'rejected') {
|
||||
errors.push({
|
||||
context: 'Roles update',
|
||||
apiError: toSaveApiError(rolesResult.reason),
|
||||
onRetry: retryRolesUpdate,
|
||||
});
|
||||
} else {
|
||||
for (const failure of rolesResult.value) {
|
||||
const context = `Role '${failure.roleName}'`;
|
||||
errors.push({
|
||||
context,
|
||||
apiError: toSaveApiError(failure.error),
|
||||
onRetry: makeRoleRetry(context, failure.onRetry),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
setSaveErrors(errors);
|
||||
} else {
|
||||
toast.success('Service account updated successfully', {
|
||||
richColors: true,
|
||||
});
|
||||
onSuccess({ closeDrawer: false });
|
||||
}
|
||||
|
||||
refetchAccount();
|
||||
queryClient.invalidateQueries(getListServiceAccountsQueryKey());
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [
|
||||
account,
|
||||
isDirty,
|
||||
localName,
|
||||
localRoles,
|
||||
availableRoles,
|
||||
updateMutateAsync,
|
||||
applyDiff,
|
||||
refetchAccount,
|
||||
onSuccess,
|
||||
queryClient,
|
||||
toSaveApiError,
|
||||
retryNameUpdate,
|
||||
makeRoleRetry,
|
||||
retryRolesUpdate,
|
||||
]);
|
||||
updateAccount({
|
||||
pathParams: { id: account.id },
|
||||
data: { name: localName, email: account.email, roles: localRoles },
|
||||
});
|
||||
}
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
setIsDeleteOpen(null);
|
||||
setIsDisableOpen(null);
|
||||
setIsAddKeyOpen(null);
|
||||
setSelectedAccountId(null);
|
||||
setActiveTab(null);
|
||||
setKeysPage(null);
|
||||
setEditKeyId(null);
|
||||
setSaveErrors([]);
|
||||
}, [
|
||||
setSelectedAccountId,
|
||||
setActiveTab,
|
||||
setKeysPage,
|
||||
setEditKeyId,
|
||||
setIsAddKeyOpen,
|
||||
setIsDeleteOpen,
|
||||
setIsDisableOpen,
|
||||
]);
|
||||
|
||||
const drawerContent = (
|
||||
@@ -377,7 +220,7 @@ function ServiceAccountDrawer({
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
disabled={isDeleted}
|
||||
disabled={isDisabled}
|
||||
onClick={(): void => {
|
||||
setIsAddKeyOpen(true);
|
||||
}}
|
||||
@@ -408,23 +251,22 @@ function ServiceAccountDrawer({
|
||||
<OverviewTab
|
||||
account={account}
|
||||
localName={localName}
|
||||
onNameChange={handleNameChange}
|
||||
onNameChange={setLocalName}
|
||||
localRoles={localRoles}
|
||||
onRolesChange={setLocalRoles}
|
||||
isDisabled={isDeleted}
|
||||
isDisabled={isDisabled}
|
||||
availableRoles={availableRoles}
|
||||
rolesLoading={rolesLoading}
|
||||
rolesError={rolesError}
|
||||
rolesErrorObj={rolesErrorObj}
|
||||
onRefetchRoles={refetchRoles}
|
||||
saveErrors={saveErrors}
|
||||
/>
|
||||
)}
|
||||
{activeTab === ServiceAccountDrawerTab.Keys && (
|
||||
<KeysTab
|
||||
keys={keys}
|
||||
isLoading={keysLoading}
|
||||
isDisabled={isDeleted}
|
||||
isDisabled={isDisabled}
|
||||
currentPage={keysPage}
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
@@ -456,20 +298,20 @@ function ServiceAccountDrawer({
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{!isDeleted && (
|
||||
{!isDisabled && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="destructive"
|
||||
className="sa-drawer__footer-btn"
|
||||
onClick={(): void => {
|
||||
setIsDeleteOpen(true);
|
||||
setIsDisableOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Delete Service Account
|
||||
<PowerOff size={12} />
|
||||
Disable Service Account
|
||||
</Button>
|
||||
)}
|
||||
{!isDeleted && (
|
||||
{!isDisabled && (
|
||||
<div className="sa-drawer__footer-right">
|
||||
<Button
|
||||
variant="solid"
|
||||
@@ -517,7 +359,7 @@ function ServiceAccountDrawer({
|
||||
className="sa-drawer"
|
||||
/>
|
||||
|
||||
<DeleteAccountModal />
|
||||
<DisableAccountModal />
|
||||
|
||||
<AddKeyModal />
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { ServiceaccounttypesFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
@@ -14,16 +14,17 @@ const mockToast = jest.mocked(toast);
|
||||
|
||||
const SA_KEY_ENDPOINT = '*/api/v1/service_accounts/sa-1/keys/key-1';
|
||||
|
||||
const mockKey: ServiceaccounttypesGettableFactorAPIKeyDTO = {
|
||||
const mockKey: ServiceaccounttypesFactorAPIKeyDTO = {
|
||||
id: 'key-1',
|
||||
name: 'Original Key Name',
|
||||
expiresAt: 0,
|
||||
lastObservedAt: null as any,
|
||||
key: 'snz_abc123',
|
||||
serviceAccountId: 'sa-1',
|
||||
};
|
||||
|
||||
function renderModal(
|
||||
keyItem: ServiceaccounttypesGettableFactorAPIKeyDTO | null = mockKey,
|
||||
keyItem: ServiceaccounttypesFactorAPIKeyDTO | null = mockKey,
|
||||
searchParams: Record<string, string> = {
|
||||
account: 'sa-1',
|
||||
'edit-key': 'key-1',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { ServiceaccounttypesFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
@@ -14,12 +14,13 @@ const mockToast = jest.mocked(toast);
|
||||
|
||||
const SA_KEY_ENDPOINT = '*/api/v1/service_accounts/sa-1/keys/:fid';
|
||||
|
||||
const keys: ServiceaccounttypesGettableFactorAPIKeyDTO[] = [
|
||||
const keys: ServiceaccounttypesFactorAPIKeyDTO[] = [
|
||||
{
|
||||
id: 'key-1',
|
||||
name: 'Production Key',
|
||||
expiresAt: 0,
|
||||
lastObservedAt: null as any,
|
||||
key: 'snz_prod_123',
|
||||
serviceAccountId: 'sa-1',
|
||||
},
|
||||
{
|
||||
@@ -27,6 +28,7 @@ const keys: ServiceaccounttypesGettableFactorAPIKeyDTO[] = [
|
||||
name: 'Staging Key',
|
||||
expiresAt: 1924905600, // 2030-12-31
|
||||
lastObservedAt: new Date('2026-03-10T10:00:00Z'),
|
||||
key: 'snz_stag_456',
|
||||
serviceAccountId: 'sa-1',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -23,9 +23,7 @@ jest.mock('@signozhq/sonner', () => ({
|
||||
const ROLES_ENDPOINT = '*/api/v1/roles';
|
||||
const SA_KEYS_ENDPOINT = '*/api/v1/service_accounts/:id/keys';
|
||||
const SA_ENDPOINT = '*/api/v1/service_accounts/sa-1';
|
||||
const SA_DELETE_ENDPOINT = '*/api/v1/service_accounts/sa-1';
|
||||
const SA_ROLES_ENDPOINT = '*/api/v1/service_accounts/:id/roles';
|
||||
const SA_ROLE_DELETE_ENDPOINT = '*/api/v1/service_accounts/:id/roles/:rid';
|
||||
const SA_STATUS_ENDPOINT = '*/api/v1/service_accounts/sa-1/status';
|
||||
|
||||
const activeAccountResponse = {
|
||||
id: 'sa-1',
|
||||
@@ -37,10 +35,10 @@ const activeAccountResponse = {
|
||||
updatedAt: '2026-01-02T00:00:00Z',
|
||||
};
|
||||
|
||||
const deletedAccountResponse = {
|
||||
const disabledAccountResponse = {
|
||||
...activeAccountResponse,
|
||||
id: 'sa-2',
|
||||
status: 'DELETED',
|
||||
status: 'DISABLED',
|
||||
};
|
||||
|
||||
function renderDrawer(
|
||||
@@ -69,23 +67,7 @@ describe('ServiceAccountDrawer', () => {
|
||||
rest.put(SA_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
rest.delete(SA_DELETE_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
rest.get(SA_ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: listRolesSuccessResponse.data.filter(
|
||||
(r) => r.name === 'signoz-admin',
|
||||
),
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.post(SA_ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) =>
|
||||
rest.put(SA_STATUS_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
);
|
||||
@@ -133,6 +115,8 @@ describe('ServiceAccountDrawer', () => {
|
||||
expect(updateSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'CI Bot Updated',
|
||||
email: 'ci-bot@signoz.io',
|
||||
roles: ['signoz-admin'],
|
||||
}),
|
||||
);
|
||||
expect(onSuccess).toHaveBeenCalledWith({ closeDrawer: false });
|
||||
@@ -141,7 +125,6 @@ describe('ServiceAccountDrawer', () => {
|
||||
|
||||
it('changing roles enables Save; clicking Save sends updated roles in payload', async () => {
|
||||
const updateSpy = jest.fn();
|
||||
const roleSpy = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
@@ -149,10 +132,6 @@ describe('ServiceAccountDrawer', () => {
|
||||
updateSpy(await req.json());
|
||||
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
|
||||
}),
|
||||
rest.post(SA_ROLES_ENDPOINT, async (req, res, ctx) => {
|
||||
roleSpy(await req.json());
|
||||
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
|
||||
}),
|
||||
);
|
||||
|
||||
renderDrawer();
|
||||
@@ -167,22 +146,21 @@ describe('ServiceAccountDrawer', () => {
|
||||
await user.click(saveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateSpy).not.toHaveBeenCalled();
|
||||
expect(roleSpy).toHaveBeenCalledWith(
|
||||
expect(updateSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: '019c24aa-2248-7585-a129-4188b3473c27',
|
||||
roles: expect.arrayContaining(['signoz-admin', 'signoz-viewer']),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('"Delete Service Account" opens confirm dialog; confirming sends delete request', async () => {
|
||||
const deleteSpy = jest.fn();
|
||||
it('"Disable Service Account" opens confirm dialog; confirming sends correct status payload', async () => {
|
||||
const statusSpy = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.delete(SA_DELETE_ENDPOINT, (_, res, ctx) => {
|
||||
deleteSpy();
|
||||
rest.put(SA_STATUS_ENDPOINT, async (req, res, ctx) => {
|
||||
statusSpy(await req.json());
|
||||
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
|
||||
}),
|
||||
);
|
||||
@@ -192,19 +170,19 @@ describe('ServiceAccountDrawer', () => {
|
||||
await screen.findByDisplayValue('CI Bot');
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /Delete Service Account/i }),
|
||||
screen.getByRole('button', { name: /Disable Service Account/i }),
|
||||
);
|
||||
|
||||
const dialog = await screen.findByRole('dialog', {
|
||||
name: /Delete service account CI Bot/i,
|
||||
name: /Disable service account CI Bot/i,
|
||||
});
|
||||
expect(dialog).toBeInTheDocument();
|
||||
|
||||
const confirmBtns = screen.getAllByRole('button', { name: /^Delete$/i });
|
||||
const confirmBtns = screen.getAllByRole('button', { name: /^Disable$/i });
|
||||
await user.click(confirmBtns[confirmBtns.length - 1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteSpy).toHaveBeenCalled();
|
||||
expect(statusSpy).toHaveBeenCalledWith({ status: 'DISABLED' });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -212,17 +190,14 @@ describe('ServiceAccountDrawer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deleted account shows read-only name, no Save button, no Delete button', async () => {
|
||||
it('disabled account shows read-only name, no Save button, no Disable button', async () => {
|
||||
server.use(
|
||||
rest.get('*/api/v1/service_accounts/sa-2', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ data: deletedAccountResponse })),
|
||||
res(ctx.status(200), ctx.json({ data: disabledAccountResponse })),
|
||||
),
|
||||
rest.get('*/api/v1/service_accounts/sa-2/keys', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ data: [] })),
|
||||
),
|
||||
rest.get('*/api/v1/service_accounts/sa-2/roles', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ data: [] })),
|
||||
),
|
||||
);
|
||||
|
||||
renderDrawer({ account: 'sa-2' });
|
||||
@@ -233,7 +208,7 @@ describe('ServiceAccountDrawer', () => {
|
||||
screen.queryByRole('button', { name: /Save Changes/i }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /Delete Service Account/i }),
|
||||
screen.queryByRole('button', { name: /Disable Service Account/i }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByDisplayValue('CI Bot')).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -273,169 +248,3 @@ describe('ServiceAccountDrawer', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ServiceAccountDrawer – save-error UX', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
server.use(
|
||||
rest.get(ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
|
||||
),
|
||||
rest.get(SA_KEYS_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ data: [] })),
|
||||
),
|
||||
rest.get(SA_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ data: activeAccountResponse })),
|
||||
),
|
||||
rest.put(SA_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
rest.delete(SA_DELETE_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
rest.get(SA_ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: listRolesSuccessResponse.data.filter(
|
||||
(r) => r.name === 'signoz-admin',
|
||||
),
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.post(SA_ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('name update failure shows SaveErrorItem with "Name update" context', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.put(SA_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(500),
|
||||
ctx.json({
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'name update failed',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
renderDrawer();
|
||||
|
||||
const nameInput = await screen.findByDisplayValue('CI Bot');
|
||||
await user.clear(nameInput);
|
||||
await user.type(nameInput, 'New Name');
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: /Save Changes/i });
|
||||
await waitFor(() => expect(saveBtn).not.toBeDisabled());
|
||||
await user.click(saveBtn);
|
||||
|
||||
expect(
|
||||
await screen.findByText(/Name update.*name update failed/i, undefined, {
|
||||
timeout: 5000,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('role update failure shows SaveErrorItem with the role name context', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(SA_ROLES_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(500),
|
||||
ctx.json({
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'role assign failed',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
renderDrawer();
|
||||
|
||||
await screen.findByDisplayValue('CI Bot');
|
||||
|
||||
// Add the signoz-viewer role (which is not currently assigned)
|
||||
await user.click(screen.getByLabelText('Roles'));
|
||||
await user.click(await screen.findByTitle('signoz-viewer'));
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: /Save Changes/i });
|
||||
await waitFor(() => expect(saveBtn).not.toBeDisabled());
|
||||
await user.click(saveBtn);
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
/Role 'signoz-viewer'.*role assign failed/i,
|
||||
undefined,
|
||||
{
|
||||
timeout: 5000,
|
||||
},
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clicking Retry on a name-update error re-triggers the request; on success the error item is removed', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
// First: PUT always fails so the error appears
|
||||
server.use(
|
||||
rest.put(SA_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(500),
|
||||
ctx.json({
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'name update failed',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
renderDrawer();
|
||||
|
||||
const nameInput = await screen.findByDisplayValue('CI Bot');
|
||||
await user.clear(nameInput);
|
||||
await user.type(nameInput, 'Retry Test');
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: /Save Changes/i });
|
||||
await waitFor(() => expect(saveBtn).not.toBeDisabled());
|
||||
await user.click(saveBtn);
|
||||
|
||||
await screen.findByText(/Name update.*name update failed/i, undefined, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
server.use(
|
||||
rest.put(SA_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
);
|
||||
|
||||
const retryBtn = screen.getByRole('button', { name: /Retry/i });
|
||||
await user.click(retryBtn);
|
||||
|
||||
// Error item should be removed after successful retry
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText(/Name update.*name update failed/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import dayjs from 'dayjs';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
export interface SaveError {
|
||||
context: string;
|
||||
apiError: APIError;
|
||||
onRetry: () => Promise<void>;
|
||||
}
|
||||
|
||||
export enum ServiceAccountDrawerTab {
|
||||
Overview = 'overview',
|
||||
|
||||
@@ -8,6 +8,7 @@ const mockActiveAccount: ServiceAccountRow = {
|
||||
id: 'sa-1',
|
||||
name: 'CI Bot',
|
||||
email: 'ci-bot@signoz.io',
|
||||
roles: ['signoz-admin'],
|
||||
status: 'ACTIVE',
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
updatedAt: '2026-01-02T00:00:00Z',
|
||||
@@ -17,6 +18,7 @@ const mockDisabledAccount: ServiceAccountRow = {
|
||||
id: 'sa-2',
|
||||
name: 'Legacy Bot',
|
||||
email: 'legacy@signoz.io',
|
||||
roles: ['signoz-viewer', 'signoz-editor', 'billing-manager'],
|
||||
status: 'DISABLED',
|
||||
createdAt: '2025-06-01T00:00:00Z',
|
||||
updatedAt: '2025-12-01T00:00:00Z',
|
||||
@@ -37,6 +39,7 @@ describe('ServiceAccountsTable', () => {
|
||||
|
||||
expect(screen.getByText('CI Bot')).toBeInTheDocument();
|
||||
expect(screen.getByText('ci-bot@signoz.io')).toBeInTheDocument();
|
||||
expect(screen.getByText('signoz-admin')).toBeInTheDocument();
|
||||
expect(screen.getByText('ACTIVE')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -46,6 +49,8 @@ describe('ServiceAccountsTable', () => {
|
||||
);
|
||||
|
||||
expect(screen.getByText('DISABLED')).toBeInTheDocument();
|
||||
expect(screen.getByText('signoz-viewer')).toBeInTheDocument();
|
||||
expect(screen.getByText('+2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onRowClick with the correct account when a row is clicked', async () => {
|
||||
|
||||
@@ -25,6 +25,32 @@ export function NameEmailCell({
|
||||
);
|
||||
}
|
||||
|
||||
export function RolesCell({ roles }: { roles: string[] }): JSX.Element {
|
||||
if (!roles || roles.length === 0) {
|
||||
return <span className="sa-dash">—</span>;
|
||||
}
|
||||
const first = roles[0];
|
||||
const overflow = roles.length - 1;
|
||||
const tooltipContent = roles.slice(1).join(', ');
|
||||
|
||||
return (
|
||||
<div className="sa-roles-cell">
|
||||
<Badge color="vanilla">{first}</Badge>
|
||||
{overflow > 0 && (
|
||||
<Tooltip
|
||||
title={tooltipContent}
|
||||
overlayClassName="sa-tooltip"
|
||||
overlayStyle={{ maxWidth: '600px' }}
|
||||
>
|
||||
<Badge color="vanilla" variant="outline" className="sa-status-badge">
|
||||
+{overflow}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StatusBadge({ status }: { status: string }): JSX.Element {
|
||||
if (status?.toUpperCase() === 'ACTIVE') {
|
||||
return (
|
||||
@@ -33,16 +59,9 @@ export function StatusBadge({ status }: { status: string }): JSX.Element {
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (status?.toUpperCase() === 'DELETED') {
|
||||
return (
|
||||
<Badge color="cherry" variant="outline">
|
||||
DELETED
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge color="vanilla" variant="outline" className="sa-status-badge">
|
||||
{status ? status.toUpperCase() : 'UNKNOWN'}
|
||||
DISABLED
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
@@ -79,6 +98,13 @@ export const columns: ColumnsType<ServiceAccountRow> = [
|
||||
<NameEmailCell name={record.name} email={record.email} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Roles',
|
||||
dataIndex: 'roles',
|
||||
key: 'roles',
|
||||
width: 420,
|
||||
render: (roles: string[]): JSX.Element => <RolesCell roles={roles} />,
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
|
||||
@@ -38,6 +38,7 @@ const ROUTES = {
|
||||
SETTINGS: '/settings',
|
||||
MY_SETTINGS: '/settings/my-settings',
|
||||
ORG_SETTINGS: '/settings/org-settings',
|
||||
API_KEYS: '/settings/api-keys',
|
||||
INGESTION_SETTINGS: '/settings/ingestion-settings',
|
||||
SOMETHING_WENT_WRONG: '/something-went-wrong',
|
||||
UN_AUTHORIZED: '/un-authorized',
|
||||
|
||||
@@ -248,5 +248,15 @@ export function createShortcutActions(deps: ActionDeps): CmdAction[] {
|
||||
roles: ['ADMIN', 'EDITOR'],
|
||||
perform: (): void => navigate(ROUTES.BILLING),
|
||||
},
|
||||
{
|
||||
id: 'my-settings-api-keys',
|
||||
name: 'Go to Account Settings API Keys',
|
||||
shortcut: [GlobalShortcutsName.NavigateToSettingsAPIKeys],
|
||||
keywords: 'account settings api keys',
|
||||
section: 'Settings',
|
||||
icon: <Settings size={14} />,
|
||||
roles: ['ADMIN', 'EDITOR'],
|
||||
perform: (): void => navigate(ROUTES.API_KEYS),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export const GlobalShortcuts = {
|
||||
NavigateToSettings: 'shift+g',
|
||||
NavigateToSettingsIngestion: 'shift+g+i',
|
||||
NavigateToSettingsBilling: 'shift+g+b',
|
||||
NavigateToSettingsAPIKeys: 'shift+g+k',
|
||||
NavigateToSettingsNotificationChannels: 'shift+g+n',
|
||||
};
|
||||
|
||||
@@ -46,6 +47,7 @@ export const GlobalShortcutsName = {
|
||||
NavigateToSettings: 'shift+g',
|
||||
NavigateToSettingsIngestion: 'shift+g+i',
|
||||
NavigateToSettingsBilling: 'shift+g+b',
|
||||
NavigateToSettingsAPIKeys: 'shift+g+k',
|
||||
NavigateToSettingsNotificationChannels: 'shift+g+n',
|
||||
NavigateToLogs: 'shift+l',
|
||||
NavigateToLogsPipelines: 'shift+l+p',
|
||||
@@ -70,6 +72,7 @@ export const GlobalShortcutsDescription = {
|
||||
NavigateToSettings: 'Navigate to Settings',
|
||||
NavigateToSettingsIngestion: 'Navigate to Ingestion Settings',
|
||||
NavigateToSettingsBilling: 'Navigate to Billing Settings',
|
||||
NavigateToSettingsAPIKeys: 'Navigate to API Keys Settings',
|
||||
NavigateToSettingsNotificationChannels:
|
||||
'Navigate to Notification Channels Settings',
|
||||
NavigateToLogsPipelines: 'Navigate to Logs Pipelines',
|
||||
|
||||
685
frontend/src/container/APIKeys/APIKeys.styles.scss
Normal file
685
frontend/src/container/APIKeys/APIKeys.styles.scss
Normal file
@@ -0,0 +1,685 @@
|
||||
.api-key-container {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
.api-key-content {
|
||||
width: calc(100% - 30px);
|
||||
max-width: 736px;
|
||||
|
||||
.title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: var(--font-size-lg);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 28px; /* 155.556% */
|
||||
letter-spacing: -0.09px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.api-keys-search-add-new {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
padding: 16px 0;
|
||||
|
||||
.add-new-api-key-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-row {
|
||||
.ant-table-cell {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: var(--bg-ink-500);
|
||||
}
|
||||
.column-render {
|
||||
margin: 8px 0 !important;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
|
||||
.title-with-action {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
|
||||
.api-key-data {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.api-key-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
border-radius: 20px;
|
||||
padding: 0px 12px;
|
||||
|
||||
background: var(--bg-ink-200);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: var(--font-size-xs);
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.copy-key-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.visibility-btn {
|
||||
border: 1px solid rgba(113, 144, 249, 0.2);
|
||||
background: rgba(113, 144, 249, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-collapse {
|
||||
border: none;
|
||||
|
||||
.ant-collapse-header {
|
||||
padding: 0px 8px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #121317;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
|
||||
.ant-collapse-item {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ant-collapse-expand-icon {
|
||||
padding-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
border-top: 1px solid var(--bg-slate-500);
|
||||
padding: 8px;
|
||||
|
||||
.api-key-tag {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50px;
|
||||
background: var(--bg-slate-300);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.tag-text {
|
||||
color: var(--bg-vanilla-400);
|
||||
leading-trim: both;
|
||||
text-edge: cap;
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: normal;
|
||||
letter-spacing: -0.05px;
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-created-by {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.api-key-last-used-at {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 18px; /* 128.571% */
|
||||
letter-spacing: -0.07px;
|
||||
font-variant-numeric: lining-nums tabular-nums stacked-fractions
|
||||
slashed-zero;
|
||||
font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on;
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-expires-in {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.dot {
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: var(--bg-amber-400);
|
||||
|
||||
.dot {
|
||||
background: var(--bg-amber-400);
|
||||
box-shadow: 0px 0px 6px 0px var(--bg-amber-400);
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: var(--bg-cherry-400);
|
||||
|
||||
.dot {
|
||||
background: var(--bg-cherry-400);
|
||||
box-shadow: 0px 0px 6px 0px var(--bg-cherry-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> a {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-variant-numeric: lining-nums tabular-nums slashed-zero;
|
||||
font-feature-settings: 'dlig' on, 'salt' on, 'case' on, 'cpsp' on;
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 20px; /* 142.857% */
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination-item-active {
|
||||
background-color: var(--bg-robin-500);
|
||||
> a {
|
||||
color: var(--bg-ink-500) !important;
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-info-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-direction: column;
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.user-avatar {
|
||||
background-color: lightslategray;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.user-email {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-radius: 20px;
|
||||
padding: 0px 12px;
|
||||
background: var(--bg-ink-200);
|
||||
|
||||
font-family: 'Space Mono', monospace;
|
||||
}
|
||||
|
||||
.role {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-modal {
|
||||
.ant-modal-content {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
padding: 0;
|
||||
|
||||
.ant-modal-header {
|
||||
background: none;
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.ant-modal-close-x {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
padding: 16px;
|
||||
margin-top: 0;
|
||||
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-access-role {
|
||||
display: flex;
|
||||
|
||||
.ant-radio-button-wrapper {
|
||||
font-size: 12px;
|
||||
text-transform: capitalize;
|
||||
|
||||
&.ant-radio-button-wrapper-checked {
|
||||
color: #fff;
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
border-color: var(--bg-slate-400, #1d212d);
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
border-color: var(--bg-slate-400, #1d212d);
|
||||
|
||||
&::before {
|
||||
background-color: var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: #fff;
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
border-color: var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
|
||||
flex: 1;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&::before {
|
||||
background: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
}
|
||||
|
||||
.role {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-api-key-modal {
|
||||
width: calc(100% - 30px) !important; /* Adjust the 20px as needed */
|
||||
max-width: 384px;
|
||||
.ant-modal-content {
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.ant-modal-header {
|
||||
padding: 16px;
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 0px 16px 28px 16px;
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.api-key-input {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ant-color-picker-trigger {
|
||||
padding: 6px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
.ant-color-picker-color-block {
|
||||
border-radius: 50px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.ant-color-picker-color-block-inner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 16px 16px;
|
||||
margin: 0;
|
||||
|
||||
.cancel-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-slate-500);
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-cherry-500);
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
color: var(--bg-vanilla-100);
|
||||
background: var(--bg-cherry-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 20px; /* 142.857% */
|
||||
}
|
||||
}
|
||||
|
||||
.expiration-selector {
|
||||
.ant-select-selector {
|
||||
border: 1px solid var(--bg-slate-400) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.newAPIKeyDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.copyable-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-radius: 20px;
|
||||
padding: 0px 12px;
|
||||
background: var(--bg-ink-200, #23262e);
|
||||
|
||||
.copy-key-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.api-key-container {
|
||||
.api-key-content {
|
||||
.title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.ant-table-row {
|
||||
.ant-table-cell {
|
||||
background: var(--bg-vanilla-200);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.ant-table-cell {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.column-render {
|
||||
border: 1px solid var(--bg-vanilla-200);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.ant-collapse {
|
||||
border: none;
|
||||
|
||||
.ant-collapse-header {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
.title-with-action {
|
||||
.api-key-title {
|
||||
.ant-typography {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-value {
|
||||
background: var(--bg-vanilla-200);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
.copy-key-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
.ant-typography {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-details {
|
||||
border-top: 1px solid var(--bg-vanilla-200);
|
||||
.api-key-tag {
|
||||
background: var(--bg-vanilla-200);
|
||||
.tag-text {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-created-by {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.api-key-last-used-at {
|
||||
.ant-typography {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-api-key-modal {
|
||||
.ant-modal-content {
|
||||
border: 1px solid var(--bg-vanilla-200);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.ant-modal-header {
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
.ant-typography {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.api-key-input {
|
||||
.ant-input {
|
||||
background: var(--bg-vanilla-200);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
.cancel-btn {
|
||||
background: var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-info-container {
|
||||
.user-email {
|
||||
background: var(--bg-vanilla-200);
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-modal {
|
||||
.ant-modal-content {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-vanilla-200);
|
||||
background: var(--bg-vanilla-100);
|
||||
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
padding: 0;
|
||||
|
||||
.ant-modal-header {
|
||||
background: none;
|
||||
border-bottom: 1px solid var(--bg-vanilla-200);
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.api-key-access-role {
|
||||
.ant-radio-button-wrapper {
|
||||
&.ant-radio-button-wrapper-checked {
|
||||
color: var(--bg-ink-400);
|
||||
background: var(--bg-vanilla-300);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-ink-400);
|
||||
background: var(--bg-vanilla-300);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
|
||||
&::before {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: var(--bg-ink-400);
|
||||
background: var(--bg-vanilla-300);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
&::before {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyable-text {
|
||||
background: var(--bg-vanilla-200);
|
||||
}
|
||||
}
|
||||
99
frontend/src/container/APIKeys/APIKeys.test.tsx
Normal file
99
frontend/src/container/APIKeys/APIKeys.test.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
createAPIKeyResponse,
|
||||
getAPIKeysResponse,
|
||||
} from 'mocks-server/__mockdata__/apiKeys';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
import APIKeys from './APIKeys';
|
||||
|
||||
const apiKeysURL = 'http://localhost/api/v1/pats';
|
||||
|
||||
describe('APIKeys component', () => {
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(apiKeysURL, (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(getAPIKeysResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
render(<APIKeys />);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders APIKeys component without crashing', () => {
|
||||
expect(screen.getByText('API Keys')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Create and manage API keys for the SigNoz API'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('render list of Access Tokens', async () => {
|
||||
server.use(
|
||||
rest.get(apiKeysURL, (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(getAPIKeysResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('No Expiry Key')).toBeInTheDocument();
|
||||
expect(screen.getByText('1-5 of 18 keys')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('opens add new key modal on button click', async () => {
|
||||
fireEvent.click(screen.getByText('New Key'));
|
||||
await waitFor(() => {
|
||||
const createNewKeyBtn = screen.getByRole('button', {
|
||||
name: /Create new key/i,
|
||||
});
|
||||
|
||||
expect(createNewKeyBtn).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('closes add new key modal on cancel button click', async () => {
|
||||
fireEvent.click(screen.getByText('New Key'));
|
||||
|
||||
const createNewKeyBtn = screen.getByRole('button', {
|
||||
name: /Create new key/i,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createNewKeyBtn).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.click(screen.getByText('Cancel'));
|
||||
await waitFor(() => {
|
||||
expect(createNewKeyBtn).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a new key on form submission', async () => {
|
||||
server.use(
|
||||
rest.post(apiKeysURL, (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(createAPIKeyResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('New Key'));
|
||||
|
||||
const createNewKeyBtn = screen.getByRole('button', {
|
||||
name: /Create new key/i,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createNewKeyBtn).toBeInTheDocument();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
const inputElement = screen.getByPlaceholderText('Enter Key Name');
|
||||
fireEvent.change(inputElement, { target: { value: 'Top Secret' } });
|
||||
fireEvent.click(screen.getByTestId('create-form-admin-role-btn'));
|
||||
fireEvent.click(createNewKeyBtn);
|
||||
});
|
||||
});
|
||||
});
|
||||
874
frontend/src/container/APIKeys/APIKeys.tsx
Normal file
874
frontend/src/container/APIKeys/APIKeys.tsx
Normal file
@@ -0,0 +1,874 @@
|
||||
import { ChangeEvent, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Col,
|
||||
Collapse,
|
||||
CollapseProps,
|
||||
Flex,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Radio,
|
||||
Row,
|
||||
Select,
|
||||
Table,
|
||||
TableProps,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import createAPIKeyApi from 'api/v1/pats/create';
|
||||
import deleteAPIKeyApi from 'api/v1/pats/delete';
|
||||
import updateAPIKeyApi from 'api/v1/pats/update';
|
||||
import cx from 'classnames';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { useGetAllAPIKeys } from 'hooks/APIKeys/useGetAllAPIKeys';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import {
|
||||
CalendarClock,
|
||||
Check,
|
||||
ClipboardEdit,
|
||||
Contact2,
|
||||
Copy,
|
||||
Eye,
|
||||
Minus,
|
||||
PenLine,
|
||||
Plus,
|
||||
Search,
|
||||
Trash2,
|
||||
View,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import APIError from 'types/api/error';
|
||||
import { APIKeyProps } from 'types/api/pat/types';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import './APIKeys.styles.scss';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export const showErrorNotification = (
|
||||
notifications: NotificationInstance,
|
||||
err: APIError,
|
||||
): void => {
|
||||
notifications.error({
|
||||
message: err.getErrorCode(),
|
||||
description: err.getErrorMessage(),
|
||||
});
|
||||
};
|
||||
|
||||
type ExpiryOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const EXPIRATION_WITHIN_SEVEN_DAYS = 7;
|
||||
|
||||
const API_KEY_EXPIRY_OPTIONS: ExpiryOption[] = [
|
||||
{ value: '1', label: '1 day' },
|
||||
{ value: '7', label: '1 week' },
|
||||
{ value: '30', label: '1 month' },
|
||||
{ value: '90', label: '3 months' },
|
||||
{ value: '365', label: '1 year' },
|
||||
{ value: '0', label: 'No Expiry' },
|
||||
];
|
||||
|
||||
export const isExpiredToken = (expiryTimestamp: number): boolean => {
|
||||
if (expiryTimestamp === 0) {
|
||||
return false;
|
||||
}
|
||||
const currentTime = dayjs();
|
||||
const tokenExpiresAt = dayjs.unix(expiryTimestamp);
|
||||
return tokenExpiresAt.isBefore(currentTime);
|
||||
};
|
||||
|
||||
export const getDateDifference = (
|
||||
createdTimestamp: number,
|
||||
expiryTimestamp: number,
|
||||
): number => {
|
||||
const differenceInSeconds = Math.abs(expiryTimestamp - createdTimestamp);
|
||||
|
||||
// Convert seconds to days
|
||||
return differenceInSeconds / (60 * 60 * 24);
|
||||
};
|
||||
|
||||
function APIKeys(): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const { notifications } = useNotifications();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
const [showNewAPIKeyDetails, setShowNewAPIKeyDetails] = useState(false);
|
||||
const [, handleCopyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [activeAPIKey, setActiveAPIKey] = useState<APIKeyProps | null>();
|
||||
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [dataSource, setDataSource] = useState<APIKeyProps[]>([]);
|
||||
const { t } = useTranslation(['apiKeys']);
|
||||
|
||||
const [editForm] = Form.useForm();
|
||||
const [createForm] = Form.useForm();
|
||||
|
||||
const handleFormReset = (): void => {
|
||||
editForm.resetFields();
|
||||
createForm.resetFields();
|
||||
};
|
||||
|
||||
const hideDeleteViewModal = (): void => {
|
||||
handleFormReset();
|
||||
setActiveAPIKey(null);
|
||||
setIsDeleteModalOpen(false);
|
||||
};
|
||||
|
||||
const showDeleteModal = (apiKey: APIKeyProps): void => {
|
||||
setActiveAPIKey(apiKey);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const hideEditViewModal = (): void => {
|
||||
handleFormReset();
|
||||
setActiveAPIKey(null);
|
||||
setIsEditModalOpen(false);
|
||||
};
|
||||
|
||||
const hideAddViewModal = (): void => {
|
||||
handleFormReset();
|
||||
setShowNewAPIKeyDetails(false);
|
||||
setActiveAPIKey(null);
|
||||
setIsAddModalOpen(false);
|
||||
};
|
||||
|
||||
const showEditModal = (apiKey: APIKeyProps): void => {
|
||||
handleFormReset();
|
||||
setActiveAPIKey(apiKey);
|
||||
|
||||
editForm.setFieldsValue({
|
||||
name: apiKey.name,
|
||||
role: apiKey.role || USER_ROLES.VIEWER,
|
||||
});
|
||||
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
const showAddModal = (): void => {
|
||||
setActiveAPIKey(null);
|
||||
setIsAddModalOpen(true);
|
||||
};
|
||||
|
||||
const handleModalClose = (): void => {
|
||||
setActiveAPIKey(null);
|
||||
};
|
||||
|
||||
const {
|
||||
data: APIKeys,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
refetch: refetchAPIKeys,
|
||||
error,
|
||||
isError,
|
||||
} = useGetAllAPIKeys();
|
||||
|
||||
useEffect(() => {
|
||||
setActiveAPIKey(APIKeys?.data?.[0]);
|
||||
}, [APIKeys]);
|
||||
|
||||
useEffect(() => {
|
||||
setDataSource(APIKeys?.data || []);
|
||||
}, [APIKeys?.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
showErrorNotification(notifications, error as APIError);
|
||||
}
|
||||
}, [error, isError, notifications]);
|
||||
|
||||
const handleSearch = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setSearchValue(e.target.value);
|
||||
const filteredData = APIKeys?.data?.filter(
|
||||
(key: APIKeyProps) =>
|
||||
key &&
|
||||
key.name &&
|
||||
key.name.toLowerCase().includes(e.target.value.toLowerCase()),
|
||||
);
|
||||
setDataSource(filteredData || []);
|
||||
};
|
||||
|
||||
const clearSearch = (): void => {
|
||||
setSearchValue('');
|
||||
};
|
||||
|
||||
const { mutate: createAPIKey, isLoading: isLoadingCreateAPIKey } = useMutation(
|
||||
createAPIKeyApi,
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
setShowNewAPIKeyDetails(true);
|
||||
setActiveAPIKey(data.data);
|
||||
|
||||
refetchAPIKeys();
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as APIError);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { mutate: updateAPIKey, isLoading: isLoadingUpdateAPIKey } = useMutation(
|
||||
updateAPIKeyApi,
|
||||
{
|
||||
onSuccess: () => {
|
||||
refetchAPIKeys();
|
||||
setIsEditModalOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as APIError);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { mutate: deleteAPIKey, isLoading: isDeleteingAPIKey } = useMutation(
|
||||
deleteAPIKeyApi,
|
||||
{
|
||||
onSuccess: () => {
|
||||
refetchAPIKeys();
|
||||
setIsDeleteModalOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as APIError);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const onDeleteHandler = (): void => {
|
||||
clearSearch();
|
||||
|
||||
if (activeAPIKey) {
|
||||
deleteAPIKey(activeAPIKey.id);
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateApiKey = (): void => {
|
||||
editForm
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
if (activeAPIKey) {
|
||||
updateAPIKey({
|
||||
id: activeAPIKey.id,
|
||||
data: {
|
||||
name: values.name,
|
||||
role: values.role,
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.error('error info', errorInfo);
|
||||
});
|
||||
};
|
||||
|
||||
const onCreateAPIKey = (): void => {
|
||||
createForm
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
if (user) {
|
||||
createAPIKey({
|
||||
name: values.name,
|
||||
expiresInDays: parseInt(values.expiration, 10),
|
||||
role: values.role,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.error('error info', errorInfo);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyKey = (text: string): void => {
|
||||
handleCopyToClipboard(text);
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
};
|
||||
|
||||
const getFormattedTime = (epochTime: number): string => {
|
||||
const timeOptions: Intl.DateTimeFormatOptions = {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
};
|
||||
const formattedTime = new Date(epochTime * 1000).toLocaleTimeString(
|
||||
'en-US',
|
||||
timeOptions,
|
||||
);
|
||||
|
||||
const dateOptions: Intl.DateTimeFormatOptions = {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
};
|
||||
|
||||
const formattedDate = new Date(epochTime * 1000).toLocaleDateString(
|
||||
'en-US',
|
||||
dateOptions,
|
||||
);
|
||||
|
||||
return `${formattedDate} ${formattedTime}`;
|
||||
};
|
||||
|
||||
const handleCopyClose = (): void => {
|
||||
if (activeAPIKey) {
|
||||
handleCopyKey(activeAPIKey?.token);
|
||||
}
|
||||
|
||||
hideAddViewModal();
|
||||
};
|
||||
|
||||
const columns: TableProps<APIKeyProps>['columns'] = [
|
||||
{
|
||||
title: 'API Key',
|
||||
key: 'api-key',
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
render: (APIKey: APIKeyProps): JSX.Element => {
|
||||
const formattedDateAndTime =
|
||||
APIKey && APIKey?.lastUsed && APIKey?.lastUsed !== 0
|
||||
? getFormattedTime(APIKey?.lastUsed)
|
||||
: 'Never';
|
||||
|
||||
const createdOn = new Date(APIKey.createdAt).toLocaleString();
|
||||
|
||||
const expiresIn =
|
||||
APIKey.expiresAt === 0
|
||||
? Number.POSITIVE_INFINITY
|
||||
: getDateDifference(
|
||||
new Date(APIKey?.createdAt).getTime() / 1000,
|
||||
APIKey?.expiresAt,
|
||||
);
|
||||
|
||||
const isExpired = isExpiredToken(APIKey.expiresAt);
|
||||
|
||||
const expiresOn =
|
||||
!APIKey.expiresAt || APIKey.expiresAt === 0
|
||||
? 'No Expiry'
|
||||
: getFormattedTime(APIKey.expiresAt);
|
||||
|
||||
const updatedOn =
|
||||
!APIKey.updatedAt || APIKey.updatedAt === ''
|
||||
? null
|
||||
: new Date(APIKey.updatedAt).toLocaleString();
|
||||
|
||||
const items: CollapseProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<div className="title-with-action">
|
||||
<div className="api-key-data">
|
||||
<div className="api-key-title">
|
||||
<Typography.Text>{APIKey?.name}</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className="api-key-value">
|
||||
<Typography.Text>
|
||||
{APIKey?.token.substring(0, 2)}********
|
||||
{APIKey?.token.substring(APIKey.token.length - 2).trim()}
|
||||
</Typography.Text>
|
||||
|
||||
<Copy
|
||||
className="copy-key-btn"
|
||||
size={12}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleCopyKey(APIKey.token);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{APIKey.role === USER_ROLES.ADMIN && (
|
||||
<Tooltip title={USER_ROLES.ADMIN}>
|
||||
<Contact2 size={14} color={Color.BG_ROBIN_400} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{APIKey.role === USER_ROLES.EDITOR && (
|
||||
<Tooltip title={USER_ROLES.EDITOR}>
|
||||
<ClipboardEdit size={14} color={Color.BG_ROBIN_400} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{APIKey.role === USER_ROLES.VIEWER && (
|
||||
<Tooltip title={USER_ROLES.VIEWER}>
|
||||
<View size={14} color={Color.BG_ROBIN_400} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!APIKey.role && (
|
||||
<Tooltip title={USER_ROLES.ADMIN}>
|
||||
<Contact2 size={14} color={Color.BG_ROBIN_400} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="action-btn">
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<PenLine size={14} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showEditModal(APIKey);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showDeleteModal(APIKey);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
children: (
|
||||
<div className="api-key-info-container">
|
||||
{APIKey?.createdByUser && (
|
||||
<Row>
|
||||
<Col span={6}> Creator </Col>
|
||||
<Col span={12} className="user-info">
|
||||
<Avatar className="user-avatar" size="small">
|
||||
{APIKey?.createdByUser?.displayName?.substring(0, 1)}
|
||||
</Avatar>
|
||||
|
||||
<Typography.Text>
|
||||
{APIKey.createdByUser?.displayName}
|
||||
</Typography.Text>
|
||||
|
||||
<div className="user-email">{APIKey.createdByUser?.email}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row>
|
||||
<Col span={6}> Created on </Col>
|
||||
<Col span={12}>
|
||||
<Typography.Text>{createdOn}</Typography.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
{updatedOn && (
|
||||
<Row>
|
||||
<Col span={6}> Updated on </Col>
|
||||
<Col span={12}>
|
||||
<Typography.Text>{updatedOn}</Typography.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Row>
|
||||
<Col span={6}> Expires on </Col>
|
||||
<Col span={12}>
|
||||
<Typography.Text>{expiresOn}</Typography.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="column-render">
|
||||
<Collapse items={items} />
|
||||
|
||||
<div className="api-key-details">
|
||||
<div className="api-key-last-used-at">
|
||||
<CalendarClock size={14} />
|
||||
Last used <Minus size={12} />
|
||||
<Typography.Text>{formattedDateAndTime}</Typography.Text>
|
||||
</div>
|
||||
|
||||
{!isExpired && expiresIn <= EXPIRATION_WITHIN_SEVEN_DAYS && (
|
||||
<div
|
||||
className={cx(
|
||||
'api-key-expires-in',
|
||||
expiresIn <= 3 ? 'danger' : 'warning',
|
||||
)}
|
||||
>
|
||||
<span className="dot" /> Expires {dayjs().to(expiresOn)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isExpired && (
|
||||
<div className={cx('api-key-expires-in danger')}>
|
||||
<span className="dot" /> Expired
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="api-key-container">
|
||||
<div className="api-key-content">
|
||||
<header>
|
||||
<Typography.Title className="title">API Keys</Typography.Title>
|
||||
<Typography.Text className="subtitle">
|
||||
Create and manage API keys for the SigNoz API
|
||||
</Typography.Text>
|
||||
</header>
|
||||
|
||||
<div className="api-keys-search-add-new">
|
||||
<Input
|
||||
placeholder="Search for keys..."
|
||||
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
|
||||
value={searchValue}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="add-new-api-key-btn"
|
||||
type="primary"
|
||||
onClick={showAddModal}
|
||||
>
|
||||
<Plus size={14} /> New Key
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
loading={isLoading || isRefetching}
|
||||
showHeader={false}
|
||||
pagination={{
|
||||
pageSize: 5,
|
||||
hideOnSinglePage: true,
|
||||
showTotal: (total: number, range: number[]): string =>
|
||||
`${range[0]}-${range[1]} of ${total} keys`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Delete Key Modal */}
|
||||
<Modal
|
||||
className="delete-api-key-modal"
|
||||
title={<span className="title">Delete Key</span>}
|
||||
open={isDeleteModalOpen}
|
||||
closable
|
||||
afterClose={handleModalClose}
|
||||
onCancel={hideDeleteViewModal}
|
||||
destroyOnClose
|
||||
footer={[
|
||||
<Button
|
||||
key="cancel"
|
||||
onClick={hideDeleteViewModal}
|
||||
className="cancel-btn"
|
||||
icon={<X size={16} />}
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
icon={<Trash2 size={16} />}
|
||||
loading={isDeleteingAPIKey}
|
||||
onClick={onDeleteHandler}
|
||||
className="delete-btn"
|
||||
>
|
||||
Delete key
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Typography.Text className="delete-text">
|
||||
{t('delete_confirm_message', {
|
||||
keyName: activeAPIKey?.name,
|
||||
})}
|
||||
</Typography.Text>
|
||||
</Modal>
|
||||
|
||||
{/* Edit Key Modal */}
|
||||
<Modal
|
||||
className="api-key-modal"
|
||||
title="Edit key"
|
||||
open={isEditModalOpen}
|
||||
key="edit-api-key-modal"
|
||||
afterClose={handleModalClose}
|
||||
// closable
|
||||
onCancel={hideEditViewModal}
|
||||
destroyOnClose
|
||||
footer={[
|
||||
<Button
|
||||
key="cancel"
|
||||
onClick={hideEditViewModal}
|
||||
className="periscope-btn cancel-btn"
|
||||
icon={<X size={16} />}
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
className="periscope-btn primary"
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={isLoadingUpdateAPIKey}
|
||||
icon={<Check size={14} />}
|
||||
onClick={onUpdateApiKey}
|
||||
>
|
||||
Update key
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
name="edit-api-key-form"
|
||||
key={activeAPIKey?.id}
|
||||
form={editForm}
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
initialValues={{
|
||||
name: activeAPIKey?.name,
|
||||
role: activeAPIKey?.role,
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{ required: true }, { type: 'string', min: 6 }]}
|
||||
>
|
||||
<Input placeholder="Enter Key Name" autoFocus />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="role" label="Role">
|
||||
<Flex vertical gap="middle">
|
||||
<Radio.Group
|
||||
buttonStyle="solid"
|
||||
className="api-key-access-role"
|
||||
defaultValue={activeAPIKey?.role}
|
||||
>
|
||||
<Radio.Button value={USER_ROLES.ADMIN} className={cx('tab')}>
|
||||
<div className="role">
|
||||
<Contact2 size={14} /> Admin
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={USER_ROLES.EDITOR} className={cx('tab')}>
|
||||
<div className="role">
|
||||
<ClipboardEdit size={14} /> Editor
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={USER_ROLES.VIEWER} className={cx('tab')}>
|
||||
<div className="role">
|
||||
<Eye size={14} /> Viewer
|
||||
</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* Create New Key Modal */}
|
||||
<Modal
|
||||
className="api-key-modal"
|
||||
title="Create new key"
|
||||
open={isAddModalOpen}
|
||||
key="create-api-key-modal"
|
||||
closable
|
||||
onCancel={hideAddViewModal}
|
||||
destroyOnClose
|
||||
footer={
|
||||
showNewAPIKeyDetails
|
||||
? [
|
||||
<Button
|
||||
key="copy-key-close"
|
||||
className="periscope-btn primary"
|
||||
data-testid="copy-key-close-btn"
|
||||
type="primary"
|
||||
onClick={handleCopyClose}
|
||||
icon={<Check size={12} />}
|
||||
>
|
||||
Copy key and close
|
||||
</Button>,
|
||||
]
|
||||
: [
|
||||
<Button
|
||||
key="cancel"
|
||||
onClick={hideAddViewModal}
|
||||
className="periscope-btn cancel-btn"
|
||||
icon={<X size={16} />}
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
className="periscope-btn primary"
|
||||
test-id="create-new-key"
|
||||
key="submit"
|
||||
type="primary"
|
||||
icon={<Check size={14} />}
|
||||
loading={isLoadingCreateAPIKey}
|
||||
onClick={onCreateAPIKey}
|
||||
>
|
||||
Create new key
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
>
|
||||
{!showNewAPIKeyDetails && (
|
||||
<Form
|
||||
key="createForm"
|
||||
name="create-api-key-form"
|
||||
form={createForm}
|
||||
initialValues={{
|
||||
role: USER_ROLES.ADMIN,
|
||||
expiration: '1',
|
||||
name: '',
|
||||
}}
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{ required: true }, { type: 'string', min: 6 }]}
|
||||
validateTrigger="onFinish"
|
||||
>
|
||||
<Input placeholder="Enter Key Name" autoFocus />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="role" label="Role">
|
||||
<Flex vertical gap="middle">
|
||||
<Radio.Group
|
||||
buttonStyle="solid"
|
||||
className="api-key-access-role"
|
||||
defaultValue={USER_ROLES.ADMIN}
|
||||
>
|
||||
<Radio.Button value={USER_ROLES.ADMIN} className={cx('tab')}>
|
||||
<div className="role" data-testid="create-form-admin-role-btn">
|
||||
<Contact2 size={14} /> Admin
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={USER_ROLES.EDITOR} className="tab">
|
||||
<div className="role" data-testid="create-form-editor-role-btn">
|
||||
<ClipboardEdit size={14} /> Editor
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={USER_ROLES.VIEWER} className="tab">
|
||||
<div className="role" data-testid="create-form-viewer-role-btn">
|
||||
<Eye size={14} /> Viewer
|
||||
</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
<Form.Item name="expiration" label="Expiration">
|
||||
<Select
|
||||
className="expiration-selector"
|
||||
placeholder="Expiration"
|
||||
options={API_KEY_EXPIRY_OPTIONS}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
{showNewAPIKeyDetails && (
|
||||
<div className="api-key-info-container">
|
||||
<Row>
|
||||
<Col span={8}>Key</Col>
|
||||
<Col span={16}>
|
||||
<span className="copyable-text">
|
||||
<Typography.Text>
|
||||
{activeAPIKey?.token.substring(0, 2)}****************
|
||||
{activeAPIKey?.token.substring(activeAPIKey.token.length - 2).trim()}
|
||||
</Typography.Text>
|
||||
|
||||
<Copy
|
||||
className="copy-key-btn"
|
||||
size={12}
|
||||
onClick={(): void => {
|
||||
if (activeAPIKey) {
|
||||
handleCopyKey(activeAPIKey.token);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col span={8}>Name</Col>
|
||||
<Col span={16}>{activeAPIKey?.name}</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col span={8}>Role</Col>
|
||||
<Col span={16}>
|
||||
{activeAPIKey?.role === USER_ROLES.ADMIN && (
|
||||
<div className="role">
|
||||
<Contact2 size={14} /> Admin
|
||||
</div>
|
||||
)}
|
||||
{activeAPIKey?.role === USER_ROLES.EDITOR && (
|
||||
<div className="role">
|
||||
{' '}
|
||||
<ClipboardEdit size={14} /> Editor
|
||||
</div>
|
||||
)}
|
||||
{activeAPIKey?.role === USER_ROLES.VIEWER && (
|
||||
<div className="role">
|
||||
{' '}
|
||||
<View size={14} /> Viewer
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col span={8}>Creator</Col>
|
||||
|
||||
<Col span={16} className="user-info">
|
||||
<Avatar className="user-avatar" size="small">
|
||||
{activeAPIKey?.createdByUser?.displayName?.substring(0, 1)}
|
||||
</Avatar>
|
||||
|
||||
<Typography.Text>
|
||||
{activeAPIKey?.createdByUser?.displayName}
|
||||
</Typography.Text>
|
||||
|
||||
<div className="user-email">{activeAPIKey?.createdByUser?.email}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{activeAPIKey?.createdAt && (
|
||||
<Row>
|
||||
<Col span={8}>Created on</Col>
|
||||
<Col span={16}>
|
||||
{new Date(activeAPIKey?.createdAt).toLocaleString()}
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{activeAPIKey?.expiresAt !== 0 && activeAPIKey?.expiresAt && (
|
||||
<Row>
|
||||
<Col span={8}>Expires on</Col>
|
||||
<Col span={16}>{getFormattedTime(activeAPIKey?.expiresAt)}</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{activeAPIKey?.expiresAt === 0 && (
|
||||
<Row>
|
||||
<Col span={8}>Expires on</Col>
|
||||
<Col span={16}> No Expiry </Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default APIKeys;
|
||||
@@ -16,6 +16,7 @@ import { ORG_PREFERENCES } from 'constants/orgPreferences';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { IS_SERVICE_ACCOUNTS_ENABLED } from 'container/ServiceAccountsSettings/config';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -264,21 +265,23 @@ export default function Home(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="home-container">
|
||||
<PersistedAnnouncementBanner
|
||||
type="warning"
|
||||
storageKey={LOCALSTORAGE.DISMISSED_API_KEYS_DEPRECATION_BANNER}
|
||||
message={
|
||||
<>
|
||||
<strong>API Keys</strong> have been deprecated and replaced by{' '}
|
||||
<strong>Service Accounts</strong>. Please migrate to Service Accounts for
|
||||
programmatic API access.
|
||||
</>
|
||||
}
|
||||
action={{
|
||||
label: 'Go to Service Accounts',
|
||||
onClick: (): void => history.push(ROUTES.SERVICE_ACCOUNTS_SETTINGS),
|
||||
}}
|
||||
/>
|
||||
{IS_SERVICE_ACCOUNTS_ENABLED && (
|
||||
<PersistedAnnouncementBanner
|
||||
type="warning"
|
||||
storageKey={LOCALSTORAGE.DISMISSED_API_KEYS_DEPRECATION_BANNER}
|
||||
message={
|
||||
<>
|
||||
<strong>API Keys</strong> have been deprecated and replaced by{' '}
|
||||
<strong>Service Accounts</strong>. Please migrate to Service Accounts for
|
||||
programmatic API access.
|
||||
</>
|
||||
}
|
||||
action={{
|
||||
label: 'Go to Service Accounts',
|
||||
onClick: (): void => history.push(ROUTES.SERVICE_ACCOUNTS_SETTINGS),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="sticky-header">
|
||||
<Header
|
||||
|
||||
@@ -284,15 +284,6 @@ export default function TableViewActions(
|
||||
error,
|
||||
);
|
||||
}
|
||||
// If the value is valid JSON (object or array), pretty-print it for copying
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
}
|
||||
} catch {
|
||||
// not JSON, return as-is
|
||||
}
|
||||
return text;
|
||||
}, [fieldData.value]);
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Check, ChevronDown, Plus } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown } from 'antd';
|
||||
import { useListServiceAccounts } from 'api/generated/services/serviceaccount';
|
||||
import {
|
||||
getGetServiceAccountQueryKey,
|
||||
useListServiceAccounts,
|
||||
} from 'api/generated/services/serviceaccount';
|
||||
import type {
|
||||
GetServiceAccount200,
|
||||
ListServiceAccounts200,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import CreateServiceAccountModal from 'components/CreateServiceAccountModal/CreateServiceAccountModal';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import ServiceAccountDrawer from 'components/ServiceAccountDrawer/ServiceAccountDrawer';
|
||||
@@ -51,13 +59,29 @@ function ServiceAccountsSettings(): JSX.Element {
|
||||
parseAsBoolean.withDefault(false),
|
||||
);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const seedAccountCache = useCallback(
|
||||
(data: ListServiceAccounts200) => {
|
||||
data.data.forEach((account) => {
|
||||
queryClient.setQueryData<GetServiceAccount200>(
|
||||
getGetServiceAccountQueryKey({ id: account.id }),
|
||||
(old) => old ?? { data: account, status: data.status },
|
||||
);
|
||||
});
|
||||
},
|
||||
[queryClient],
|
||||
);
|
||||
|
||||
const {
|
||||
data: serviceAccountsData,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
refetch: handleCreateSuccess,
|
||||
} = useListServiceAccounts();
|
||||
} = useListServiceAccounts({
|
||||
query: { onSuccess: seedAccountCache },
|
||||
});
|
||||
|
||||
const allAccounts = useMemo(
|
||||
(): ServiceAccountRow[] =>
|
||||
@@ -73,10 +97,10 @@ function ServiceAccountsSettings(): JSX.Element {
|
||||
[allAccounts],
|
||||
);
|
||||
|
||||
const deletedCount = useMemo(
|
||||
const disabledCount = useMemo(
|
||||
() =>
|
||||
allAccounts.filter(
|
||||
(a) => a.status?.toUpperCase() === ServiceAccountStatus.Deleted,
|
||||
(a) => a.status?.toUpperCase() !== ServiceAccountStatus.Active,
|
||||
).length,
|
||||
[allAccounts],
|
||||
);
|
||||
@@ -88,9 +112,9 @@ function ServiceAccountsSettings(): JSX.Element {
|
||||
result = result.filter(
|
||||
(a) => a.status?.toUpperCase() === ServiceAccountStatus.Active,
|
||||
);
|
||||
} else if (filterMode === FilterMode.Deleted) {
|
||||
} else if (filterMode === FilterMode.Disabled) {
|
||||
result = result.filter(
|
||||
(a) => a.status?.toUpperCase() === ServiceAccountStatus.Deleted,
|
||||
(a) => a.status?.toUpperCase() !== ServiceAccountStatus.Active,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,7 +122,9 @@ function ServiceAccountsSettings(): JSX.Element {
|
||||
const q = searchQuery.trim().toLowerCase();
|
||||
result = result.filter(
|
||||
(a) =>
|
||||
a.name?.toLowerCase().includes(q) || a.email?.toLowerCase().includes(q),
|
||||
a.name?.toLowerCase().includes(q) ||
|
||||
a.email?.toLowerCase().includes(q) ||
|
||||
a.roles?.some((role: string) => role.toLowerCase().includes(q)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,15 +174,15 @@ function ServiceAccountsSettings(): JSX.Element {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: FilterMode.Deleted,
|
||||
key: FilterMode.Disabled,
|
||||
label: (
|
||||
<div className="sa-settings-filter-option">
|
||||
<span>Deleted ⎯ {deletedCount}</span>
|
||||
{filterMode === FilterMode.Deleted && <Check size={14} />}
|
||||
<span>Disabled ⎯ {disabledCount}</span>
|
||||
{filterMode === FilterMode.Disabled && <Check size={14} />}
|
||||
</div>
|
||||
),
|
||||
onClick: (): void => {
|
||||
setFilterMode(FilterMode.Deleted);
|
||||
setFilterMode(FilterMode.Disabled);
|
||||
setPage(1);
|
||||
},
|
||||
},
|
||||
@@ -166,8 +192,8 @@ function ServiceAccountsSettings(): JSX.Element {
|
||||
switch (filterMode) {
|
||||
case FilterMode.Active:
|
||||
return `Active ⎯ ${activeCount}`;
|
||||
case FilterMode.Deleted:
|
||||
return `Deleted ⎯ ${deletedCount}`;
|
||||
case FilterMode.Disabled:
|
||||
return `Disabled ⎯ ${disabledCount}`;
|
||||
default:
|
||||
return `All accounts ⎯ ${totalCount}`;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ReactNode } from 'react';
|
||||
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import ServiceAccountsSettings from '../ServiceAccountsSettings';
|
||||
|
||||
@@ -149,7 +149,7 @@ describe('ServiceAccountsSettings (integration)', () => {
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', { name: /Delete Service Account/i }),
|
||||
await screen.findByRole('button', { name: /Disable Service Account/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -187,16 +187,14 @@ describe('ServiceAccountsSettings (integration)', () => {
|
||||
await user.click(screen.getByRole('button', { name: /Save Changes/i }));
|
||||
|
||||
await screen.findByDisplayValue('CI Bot Updated');
|
||||
await waitFor(() => {
|
||||
expect(listRefetchSpy).toHaveBeenCalled();
|
||||
});
|
||||
expect(listRefetchSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('"New Service Account" button opens the Create Service Account modal', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<NuqsTestingAdapter hasMemory>
|
||||
<NuqsTestingAdapter>
|
||||
<ServiceAccountsSettings />
|
||||
</NuqsTestingAdapter>,
|
||||
);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const IS_SERVICE_ACCOUNTS_ENABLED = false;
|
||||
|
||||
@@ -9,5 +9,5 @@ export const SA_QUERY_PARAMS = {
|
||||
ADD_KEY: 'add-key',
|
||||
EDIT_KEY: 'edit-key',
|
||||
REVOKE_KEY: 'revoke-key',
|
||||
DELETE_SA: 'delete-sa',
|
||||
DISABLE_SA: 'disable-sa',
|
||||
} as const;
|
||||
|
||||
@@ -8,6 +8,7 @@ export function toServiceAccountRow(
|
||||
id: sa.id,
|
||||
name: sa.name,
|
||||
email: sa.email,
|
||||
roles: sa.roles,
|
||||
status: sa.status,
|
||||
createdAt: toISOString(sa.createdAt),
|
||||
updatedAt: toISOString(sa.updatedAt),
|
||||
@@ -17,18 +18,19 @@ export function toServiceAccountRow(
|
||||
export enum FilterMode {
|
||||
All = 'all',
|
||||
Active = 'active',
|
||||
Deleted = 'deleted',
|
||||
Disabled = 'disabled',
|
||||
}
|
||||
|
||||
export enum ServiceAccountStatus {
|
||||
Active = 'ACTIVE',
|
||||
Deleted = 'DELETED',
|
||||
Disabled = 'DISABLED',
|
||||
}
|
||||
|
||||
export interface ServiceAccountRow {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
roles: string[];
|
||||
status: string;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
|
||||
@@ -692,6 +692,9 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
registerShortcut(GlobalShortcuts.NavigateToSettingsBilling, () =>
|
||||
onClickHandler(ROUTES.BILLING, null),
|
||||
);
|
||||
registerShortcut(GlobalShortcuts.NavigateToSettingsAPIKeys, () =>
|
||||
onClickHandler(ROUTES.API_KEYS, null),
|
||||
);
|
||||
registerShortcut(GlobalShortcuts.NavigateToSettingsNotificationChannels, () =>
|
||||
onClickHandler(ROUTES.ALL_CHANNELS, null),
|
||||
);
|
||||
@@ -717,6 +720,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToSettings);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToSettingsIngestion);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToSettingsBilling);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToSettingsAPIKeys);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToSettingsNotificationChannels);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToLogsPipelines);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToLogsViews);
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
Github,
|
||||
HardDrive,
|
||||
Home,
|
||||
Key,
|
||||
Keyboard,
|
||||
Layers2,
|
||||
LayoutGrid,
|
||||
@@ -365,6 +366,13 @@ export const settingsNavSections: SettingsNavSection[] = [
|
||||
isEnabled: false,
|
||||
itemKey: 'service-accounts',
|
||||
},
|
||||
{
|
||||
key: ROUTES.API_KEYS,
|
||||
label: 'API Keys',
|
||||
icon: <Key size={16} />,
|
||||
isEnabled: false,
|
||||
itemKey: 'api-keys',
|
||||
},
|
||||
{
|
||||
key: ROUTES.INGESTION_SETTINGS,
|
||||
label: 'Ingestion',
|
||||
|
||||
@@ -158,6 +158,7 @@ export const routesToSkip = [
|
||||
ROUTES.MEMBERS_SETTINGS,
|
||||
ROUTES.SERVICE_ACCOUNTS_SETTINGS,
|
||||
ROUTES.INGESTION_SETTINGS,
|
||||
ROUTES.API_KEYS,
|
||||
ROUTES.ERROR_DETAIL,
|
||||
ROUTES.LOGS_PIPELINES,
|
||||
ROUTES.BILLING,
|
||||
|
||||
14
frontend/src/hooks/APIKeys/useGetAllAPIKeys.ts
Normal file
14
frontend/src/hooks/APIKeys/useGetAllAPIKeys.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
import list from 'api/v1/pats/list';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { APIKeyProps } from 'types/api/pat/types';
|
||||
|
||||
export const useGetAllAPIKeys = (): UseQueryResult<
|
||||
SuccessResponseV2<APIKeyProps[]>,
|
||||
APIError
|
||||
> =>
|
||||
useQuery<SuccessResponseV2<APIKeyProps[]>, APIError>({
|
||||
queryKey: ['APIKeys'],
|
||||
queryFn: () => list(),
|
||||
});
|
||||
@@ -1,113 +0,0 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import {
|
||||
getGetServiceAccountRolesQueryKey,
|
||||
useCreateServiceAccountRole,
|
||||
useDeleteServiceAccountRole,
|
||||
useGetServiceAccountRoles,
|
||||
} from 'api/generated/services/serviceaccount';
|
||||
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export interface RoleUpdateFailure {
|
||||
roleName: string;
|
||||
error: unknown;
|
||||
onRetry: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface UseServiceAccountRoleManagerResult {
|
||||
currentRoles: AuthtypesRoleDTO[];
|
||||
isLoading: boolean;
|
||||
applyDiff: (
|
||||
localRoleIds: string[],
|
||||
availableRoles: AuthtypesRoleDTO[],
|
||||
) => Promise<RoleUpdateFailure[]>;
|
||||
}
|
||||
|
||||
export function useServiceAccountRoleManager(
|
||||
accountId: string,
|
||||
): UseServiceAccountRoleManagerResult {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading } = useGetServiceAccountRoles({ id: accountId });
|
||||
|
||||
const currentRoles = useMemo<AuthtypesRoleDTO[]>(() => data?.data ?? [], [
|
||||
data?.data,
|
||||
]);
|
||||
|
||||
// the retry for these mutations is safe due to being idempotent on backend
|
||||
const { mutateAsync: createRole } = useCreateServiceAccountRole();
|
||||
const { mutateAsync: deleteRole } = useDeleteServiceAccountRole();
|
||||
|
||||
const invalidateRoles = useCallback(
|
||||
() =>
|
||||
queryClient.invalidateQueries(
|
||||
getGetServiceAccountRolesQueryKey({ id: accountId }),
|
||||
),
|
||||
[accountId, queryClient],
|
||||
);
|
||||
|
||||
const applyDiff = useCallback(
|
||||
async (
|
||||
localRoleIds: string[],
|
||||
availableRoles: AuthtypesRoleDTO[],
|
||||
): Promise<RoleUpdateFailure[]> => {
|
||||
const currentRoleIds = new Set(
|
||||
currentRoles.map((r) => r.id).filter(Boolean),
|
||||
);
|
||||
const desiredRoleIds = new Set(
|
||||
localRoleIds.filter((id) => id != null && id !== ''),
|
||||
);
|
||||
|
||||
const addedRoles = availableRoles.filter(
|
||||
(r) => r.id && desiredRoleIds.has(r.id) && !currentRoleIds.has(r.id),
|
||||
);
|
||||
|
||||
const removedRoles = currentRoles.filter(
|
||||
(r) => r.id && !desiredRoleIds.has(r.id),
|
||||
);
|
||||
|
||||
const allOperations = [
|
||||
...addedRoles.map((role) => ({
|
||||
role,
|
||||
run: (): ReturnType<typeof createRole> =>
|
||||
createRole({ pathParams: { id: accountId }, data: { id: role.id } }),
|
||||
})),
|
||||
...removedRoles.map((role) => ({
|
||||
role,
|
||||
run: (): ReturnType<typeof deleteRole> =>
|
||||
deleteRole({ pathParams: { id: accountId, rid: role.id } }),
|
||||
})),
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
allOperations.map((op) => op.run()),
|
||||
);
|
||||
|
||||
await invalidateRoles();
|
||||
|
||||
const failures: RoleUpdateFailure[] = [];
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'rejected') {
|
||||
const { role, run } = allOperations[index];
|
||||
failures.push({
|
||||
roleName: role.name ?? 'unknown',
|
||||
error: result.reason,
|
||||
onRetry: async (): Promise<void> => {
|
||||
await run();
|
||||
await invalidateRoles();
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return failures;
|
||||
},
|
||||
[accountId, currentRoles, createRole, deleteRole, invalidateRoles],
|
||||
);
|
||||
|
||||
return {
|
||||
currentRoles,
|
||||
isLoading,
|
||||
applyDiff,
|
||||
};
|
||||
}
|
||||
541
frontend/src/mocks-server/__mockdata__/apiKeys.ts
Normal file
541
frontend/src/mocks-server/__mockdata__/apiKeys.ts
Normal file
@@ -0,0 +1,541 @@
|
||||
const createdByEmail = 'mando@signoz.io';
|
||||
|
||||
export const getAPIKeysResponse = {
|
||||
status: 'success',
|
||||
data: [
|
||||
{
|
||||
id: '26',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: 'T2DuASwpuUx3wlYraFl5r7N9G1ikBhzGuy2ihcIKDMs=',
|
||||
role: 'ADMIN',
|
||||
name: '1 Day Old',
|
||||
createdAt: 1708010258,
|
||||
expiresAt: 1708096658,
|
||||
updatedAt: 1708010258,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '24',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
token: 'EteVs77BA4FFLJD/TsFE9c+CLX4kXVmlx+0GGK7dpXY=',
|
||||
role: 'ADMIN',
|
||||
name: '1 year expiry - updated',
|
||||
createdAt: 1708008146,
|
||||
expiresAt: 1739544146,
|
||||
updatedAt: 1708008239,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: 'mandalorian',
|
||||
},
|
||||
{
|
||||
id: '25',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
token: '1udrUFmRI6gdb8r/hLabS7zRlgfMQlUw/tz9sac82pE=',
|
||||
role: 'ADMIN',
|
||||
name: 'No Expiry Key',
|
||||
createdAt: 1708008178,
|
||||
expiresAt: 0,
|
||||
updatedAt: 1708008190,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: 'mandalorian',
|
||||
},
|
||||
{
|
||||
id: '22',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
token: 'gtqKF7g7avoe+Yu2+WhyDDLQSr6IsVaR5xpby2XhLAY=',
|
||||
role: 'VIEWER',
|
||||
name: 'No Expiry',
|
||||
createdAt: 1708007395,
|
||||
expiresAt: 0,
|
||||
updatedAt: 1708007936,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: 'mandalorian',
|
||||
},
|
||||
{
|
||||
id: '23',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
token: 'GM/TqEID8N4ynlvQHK38ITEvRAcn5XkJZpmd11xT3OQ=',
|
||||
role: 'VIEWER',
|
||||
name: 'No Expiry - 2',
|
||||
createdAt: 1708007685,
|
||||
expiresAt: 0,
|
||||
updatedAt: 1708007786,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: 'mandalorian',
|
||||
},
|
||||
{
|
||||
id: '19',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
token: 'Oj75e6Zr7JmjFcWIo0UK/Nl06RdC2BKOr/QVHoBA0gM=',
|
||||
role: 'ADMIN',
|
||||
name: '7 Days',
|
||||
createdAt: 1708003326,
|
||||
expiresAt: 1708608126,
|
||||
updatedAt: 1708007380,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: 'mandalorian',
|
||||
},
|
||||
{
|
||||
id: '20',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
token: 'T+sNdYe6I74ya/9mEKqB3UTrFm8+jwI0DiirqEx3bsM=',
|
||||
role: 'EDITOR',
|
||||
name: '1 month',
|
||||
createdAt: 1708004012,
|
||||
expiresAt: 1710596012,
|
||||
updatedAt: 1708005206,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: 'mandalorian',
|
||||
},
|
||||
{
|
||||
id: '21',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
token: 'JWw26FuymeHq+fsfFcb+2+Ls/MdokmeXxXdZisuaVeI=',
|
||||
role: 'ADMIN',
|
||||
name: '3 Months',
|
||||
createdAt: 1708004755,
|
||||
expiresAt: 1715780755,
|
||||
updatedAt: 1708005197,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: 'mandalorian',
|
||||
},
|
||||
{
|
||||
id: '17',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: '2zDrYNr+IWXUyA14+afVvO6GI9dcHfEsOYxjA9mrprg=',
|
||||
role: 'ADMIN',
|
||||
name: 'New No Expiry',
|
||||
createdAt: 1708000444,
|
||||
expiresAt: 0,
|
||||
updatedAt: 1708000444,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '14',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: 'Q+/+UB2OrDPcS9b0+5A1dDXYmWHz0abbVVidF48QCso=',
|
||||
role: 'EDITOR',
|
||||
name: 'Editor Token for user 1',
|
||||
createdAt: 1707997720,
|
||||
expiresAt: 1708170520,
|
||||
updatedAt: 1707997720,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '13',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: '/X3OEaSOLrrJImvzIB3g5WGg+5831X89fZZQT1JaxvQ=',
|
||||
role: 'EDITOR',
|
||||
name: 'Editor Token for user 2',
|
||||
createdAt: 1707997603,
|
||||
expiresAt: 1708170403,
|
||||
updatedAt: 1707997603,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: 'bTs+Q6waIiP4KJ8L5N58EQonuapWMXsfEra/cmMwmbE=',
|
||||
role: 'EDITOR',
|
||||
name: 'Editor Token for user 3',
|
||||
createdAt: 1707997539,
|
||||
expiresAt: 1708170339,
|
||||
updatedAt: 1707997539,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: 'YaEqQHrH8KOnYFllor/8Tq653TgxPU1Z7ZDzY3+ETmI=',
|
||||
role: 'EDITOR',
|
||||
name: 'Editor Token for user',
|
||||
createdAt: 1707997537,
|
||||
expiresAt: 1708170337,
|
||||
updatedAt: 1707997537,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: 'Hg/QpMU9VQyqIuzSh9ND2454IN5uOHzVkv7owEtBcPo=',
|
||||
role: 'EDITOR',
|
||||
name: 'test123',
|
||||
createdAt: 1707997288,
|
||||
expiresAt: 1708083688,
|
||||
updatedAt: 1707997288,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: 'M5gMsccDthPTibquB7kR7ZSEI76y4endOxZPESZ9/po=',
|
||||
role: 'VIEWER',
|
||||
name: 'Viewer Token for user',
|
||||
createdAt: 1707996747,
|
||||
expiresAt: 1708255947,
|
||||
updatedAt: 1707996747,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: 'H8NVlOD09IcMgQ/rzfVucb+4+jEcqZ4ZRx6n7QztMSc=',
|
||||
role: 'EDITOR',
|
||||
name: 'Editor Token for user',
|
||||
createdAt: 1707996736,
|
||||
expiresAt: 1708169536,
|
||||
updatedAt: 1707996736,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: 0,
|
||||
profilePictureURL: '',
|
||||
notFound: true,
|
||||
},
|
||||
token: 'z24SswLmNlPVUgb1j6rfc2u4Kb4xSUolwb11cI8kbrs=',
|
||||
role: 'ADMIN',
|
||||
name: 'Admin Token for user',
|
||||
createdAt: 1707996719,
|
||||
expiresAt: 0,
|
||||
updatedAt: 1707996719,
|
||||
lastUsed: 0,
|
||||
revoked: false,
|
||||
updatedByUserId: '',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
userId: 'mandalorian',
|
||||
createdByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
updatedByUser: {
|
||||
id: '001',
|
||||
name: 'Mando',
|
||||
email: createdByEmail,
|
||||
createdAt: 1707974098,
|
||||
profilePictureURL: '',
|
||||
notFound: false,
|
||||
},
|
||||
token: 'SWuNSF08EB6+VN05312QaAsPum2wkqIm+ujiWZKnm2Q=',
|
||||
role: 'EDITOR',
|
||||
name: 'Editor Token',
|
||||
createdAt: 1707992270,
|
||||
expiresAt: 1708165070,
|
||||
updatedAt: 1707995424,
|
||||
lastUsed: 1707992517,
|
||||
revoked: false,
|
||||
updatedByUserId: 'mandalorian',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const createAPIKeyResponse = {
|
||||
status: 'success',
|
||||
data: {
|
||||
id: '57',
|
||||
userId: 'mandalorian',
|
||||
token: 'pQ5kiHjcbQ2FbKlS14LQjA2RzXEBi/KvBfM7BRSwltI=',
|
||||
name: 'test1233',
|
||||
createdAt: 1707818550,
|
||||
expiresAt: 0,
|
||||
},
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import logEvent from 'api/common/logEvent';
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { IS_SERVICE_ACCOUNTS_ENABLED } from 'container/ServiceAccountsSettings/config';
|
||||
import { routeConfig } from 'container/SideNav/config';
|
||||
import { getQueryString } from 'container/SideNav/helper';
|
||||
import { settingsNavSections } from 'container/SideNav/menuItems';
|
||||
@@ -83,10 +84,12 @@ function SettingsPage(): JSX.Element {
|
||||
item.key === ROUTES.ROLES_SETTINGS ||
|
||||
item.key === ROUTES.ROLE_DETAILS ||
|
||||
item.key === ROUTES.INTEGRATIONS ||
|
||||
item.key === ROUTES.API_KEYS ||
|
||||
item.key === ROUTES.INGESTION_SETTINGS ||
|
||||
item.key === ROUTES.ORG_SETTINGS ||
|
||||
item.key === ROUTES.MEMBERS_SETTINGS ||
|
||||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS ||
|
||||
(IS_SERVICE_ACCOUNTS_ENABLED &&
|
||||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS) ||
|
||||
item.key === ROUTES.SHORTCUTS
|
||||
? true
|
||||
: item.isEnabled,
|
||||
@@ -115,9 +118,11 @@ function SettingsPage(): JSX.Element {
|
||||
item.key === ROUTES.ROLES_SETTINGS ||
|
||||
item.key === ROUTES.ROLE_DETAILS ||
|
||||
item.key === ROUTES.INTEGRATIONS ||
|
||||
item.key === ROUTES.API_KEYS ||
|
||||
item.key === ROUTES.ORG_SETTINGS ||
|
||||
item.key === ROUTES.MEMBERS_SETTINGS ||
|
||||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS ||
|
||||
(IS_SERVICE_ACCOUNTS_ENABLED &&
|
||||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS) ||
|
||||
item.key === ROUTES.INGESTION_SETTINGS
|
||||
? true
|
||||
: item.isEnabled,
|
||||
@@ -141,9 +146,11 @@ function SettingsPage(): JSX.Element {
|
||||
updatedItems = updatedItems.map((item) => ({
|
||||
...item,
|
||||
isEnabled:
|
||||
item.key === ROUTES.API_KEYS ||
|
||||
item.key === ROUTES.ORG_SETTINGS ||
|
||||
item.key === ROUTES.MEMBERS_SETTINGS ||
|
||||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS
|
||||
(IS_SERVICE_ACCOUNTS_ENABLED &&
|
||||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS)
|
||||
? true
|
||||
: item.isEnabled,
|
||||
}));
|
||||
|
||||
@@ -53,6 +53,7 @@ describe('SettingsPage nav sections', () => {
|
||||
'billing',
|
||||
'roles',
|
||||
'members',
|
||||
'api-keys',
|
||||
'sso',
|
||||
'integrations',
|
||||
'ingestion',
|
||||
@@ -81,9 +82,12 @@ describe('SettingsPage nav sections', () => {
|
||||
expect(screen.getByTestId(id)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each(['billing', 'roles'])('does not render "%s" element', (id) => {
|
||||
expect(screen.queryByTestId(id)).not.toBeInTheDocument();
|
||||
});
|
||||
it.each(['billing', 'roles', 'api-keys'])(
|
||||
'does not render "%s" element',
|
||||
(id) => {
|
||||
expect(screen.queryByTestId(id)).not.toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('Self-hosted Admin', () => {
|
||||
@@ -95,7 +99,7 @@ describe('SettingsPage nav sections', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each(['roles', 'members', 'integrations', 'sso', 'ingestion'])(
|
||||
it.each(['roles', 'members', 'api-keys', 'integrations', 'sso', 'ingestion'])(
|
||||
'renders "%s" element',
|
||||
(id) => {
|
||||
expect(screen.getByTestId(id)).toBeInTheDocument();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RouteTabProps } from 'components/RouteTab/types';
|
||||
import ROUTES from 'constants/routes';
|
||||
import AlertChannels from 'container/AllAlertChannels';
|
||||
import APIKeys from 'container/APIKeys/APIKeys';
|
||||
import BillingContainer from 'container/BillingContainer/BillingContainer';
|
||||
import CreateAlertChannels from 'container/CreateAlertChannels';
|
||||
import { ChannelType } from 'container/CreateAlertChannels/config';
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
Cpu,
|
||||
CreditCard,
|
||||
Keyboard,
|
||||
KeySquare,
|
||||
Pencil,
|
||||
Plus,
|
||||
Shield,
|
||||
@@ -112,6 +114,19 @@ export const generalSettingsCloud = (t: TFunction): RouteTabProps['routes'] => [
|
||||
},
|
||||
];
|
||||
|
||||
export const apiKeys = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: APIKeys,
|
||||
name: (
|
||||
<div className="periscope-tab">
|
||||
<KeySquare size={16} /> {t('routes:api_keys').toString()}
|
||||
</div>
|
||||
),
|
||||
route: ROUTES.API_KEYS,
|
||||
key: ROUTES.API_KEYS,
|
||||
},
|
||||
];
|
||||
|
||||
export const billingSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: BillingContainer,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { RouteTabProps } from 'components/RouteTab/types';
|
||||
import { IS_SERVICE_ACCOUNTS_ENABLED } from 'container/ServiceAccountsSettings/config';
|
||||
import { TFunction } from 'i18next';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
|
||||
import {
|
||||
alertChannels,
|
||||
apiKeys,
|
||||
billingSettings,
|
||||
createAlertChannels,
|
||||
editAlertChannels,
|
||||
@@ -62,7 +64,11 @@ export const getRoutes = (
|
||||
settings.push(...alertChannels(t));
|
||||
|
||||
if (isAdmin) {
|
||||
settings.push(...membersSettings(t), ...serviceAccountsSettings(t));
|
||||
settings.push(...apiKeys(t), ...membersSettings(t));
|
||||
|
||||
if (IS_SERVICE_ACCOUNTS_ENABLED) {
|
||||
settings.push(...serviceAccountsSettings(t));
|
||||
}
|
||||
}
|
||||
|
||||
// todo: Sagar - check the condition for role list and details page, to whom we want to serve
|
||||
|
||||
@@ -47,9 +47,6 @@ const queryClient = new QueryClient({
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
},
|
||||
mutations: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
56
frontend/src/types/api/pat/types.ts
Normal file
56
frontend/src/types/api/pat/types.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
export interface User {
|
||||
createdAt?: number;
|
||||
email?: string;
|
||||
id: string;
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
export interface APIKeyProps {
|
||||
name: string;
|
||||
expiresAt: number;
|
||||
role: string;
|
||||
token: string;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
createdByUser?: User;
|
||||
updatedAt?: string;
|
||||
updatedByUser?: User;
|
||||
lastUsed?: number;
|
||||
}
|
||||
|
||||
export interface CreatePayloadProps {
|
||||
data: APIKeyProps;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface CreateAPIKeyProps {
|
||||
name: string;
|
||||
expiresInDays: number;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface AllAPIKeyProps {
|
||||
status: string;
|
||||
data: APIKeyProps[];
|
||||
}
|
||||
|
||||
export interface CreateAPIKeyProp {
|
||||
data: APIKeyProps;
|
||||
}
|
||||
|
||||
export interface DeleteAPIKeyPayloadProps {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface UpdateAPIKeyProps {
|
||||
id: string;
|
||||
data: {
|
||||
name: string;
|
||||
role: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type PayloadProps = {
|
||||
status: string;
|
||||
data: string;
|
||||
};
|
||||
@@ -108,6 +108,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
TRACES_SAVE_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
TRACES_FUNNELS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
TRACES_FUNNELS_DETAIL: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
API_KEYS: ['ADMIN'],
|
||||
LOGS_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
OLD_LOGS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
|
||||
@@ -50,11 +50,6 @@ func (handler *healthOpenAPIHandler) ServeOpenAPI(opCtx openapi.OperationContext
|
||||
)
|
||||
}
|
||||
|
||||
func (handler *healthOpenAPIHandler) AuditDef() *pkghandler.AuditDef {
|
||||
// Health endpoints are not audited since they don't represent user actions and are called frequently by monitoring systems, which would create noise in the audit logs.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) addRegistryRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/healthz", newHealthOpenAPIHandler(
|
||||
provider.authZ.OpenAccess(provider.factoryHandler.Healthz),
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -45,23 +44,6 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/me", handler.New(provider.authZ.OpenAccess(provider.serviceAccountHandler.GetMe), handler.OpenAPIDef{
|
||||
ID: "GetMyServiceAccount",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Gets my service account",
|
||||
Description: "This endpoint gets my service account",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(serviceaccounttypes.ServiceAccountWithRoles),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: nil,
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.Get), handler.OpenAPIDef{
|
||||
ID: "GetServiceAccount",
|
||||
Tags: []string{"serviceaccount"},
|
||||
@@ -69,7 +51,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
Description: "This endpoint gets an existing service account",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(serviceaccounttypes.ServiceAccountWithRoles),
|
||||
Response: new(serviceaccounttypes.ServiceAccount),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
@@ -79,74 +61,6 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.GetRoles), handler.OpenAPIDef{
|
||||
ID: "GetServiceAccountRoles",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Gets service account roles",
|
||||
Description: "This endpoint gets all the roles for the existing service account",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new([]*authtypes.Role),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.SetRole), handler.OpenAPIDef{
|
||||
ID: "CreateServiceAccountRole",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Create service account role",
|
||||
Description: "This endpoint assigns a role to a service account",
|
||||
Request: new(serviceaccounttypes.PostableServiceAccountRole),
|
||||
RequestContentType: "",
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/roles/{rid}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.DeleteRole), handler.OpenAPIDef{
|
||||
ID: "DeleteServiceAccountRole",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Delete service account role",
|
||||
Description: "This endpoint revokes a role from service account",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/me", handler.New(provider.authZ.OpenAccess(provider.serviceAccountHandler.UpdateMe), handler.OpenAPIDef{
|
||||
ID: "UpdateMyServiceAccount",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Updates my service account",
|
||||
Description: "This endpoint gets my service account",
|
||||
Request: new(serviceaccounttypes.UpdatableServiceAccount),
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: nil,
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.Update), handler.OpenAPIDef{
|
||||
ID: "UpdateServiceAccount",
|
||||
Tags: []string{"serviceaccount"},
|
||||
@@ -164,6 +78,23 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/status", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.UpdateStatus), handler.OpenAPIDef{
|
||||
ID: "UpdateServiceAccountStatus",
|
||||
Tags: []string{"serviceaccount"},
|
||||
Summary: "Updates a service account status",
|
||||
Description: "This endpoint updates an existing service account status",
|
||||
Request: new(serviceaccounttypes.UpdatableServiceAccountStatus),
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.Delete), handler.OpenAPIDef{
|
||||
ID: "DeleteServiceAccount",
|
||||
Tags: []string{"serviceaccount"},
|
||||
@@ -205,7 +136,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
Description: "This endpoint lists the service account keys",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: make([]*serviceaccounttypes.GettableFactorAPIKey, 0),
|
||||
Response: make([]*serviceaccounttypes.FactorAPIKey, 0),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
|
||||
@@ -43,6 +43,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.ListUsersDeprecated), handler.OpenAPIDef{
|
||||
ID: "ListUsersDeprecated",
|
||||
Tags: []string{"users"},
|
||||
|
||||
@@ -21,15 +21,11 @@ func newTestSettings() factory.ScopedProviderSettings {
|
||||
|
||||
func newTestEvent(resource string, action audittypes.Action) audittypes.AuditEvent {
|
||||
return audittypes.AuditEvent{
|
||||
Timestamp: time.Now(),
|
||||
EventName: audittypes.NewEventName(resource, action),
|
||||
AuditAttributes: audittypes.AuditAttributes{
|
||||
Action: action,
|
||||
Outcome: audittypes.OutcomeSuccess,
|
||||
},
|
||||
ResourceAttributes: audittypes.ResourceAttributes{
|
||||
ResourceName: resource,
|
||||
},
|
||||
Timestamp: time.Now(),
|
||||
EventName: audittypes.NewEventName(resource, action),
|
||||
ResourceName: resource,
|
||||
Action: action,
|
||||
Outcome: audittypes.OutcomeSuccess,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ func New(store authtypes.AuthNStore) *AuthN {
|
||||
}
|
||||
|
||||
func (a *AuthN) Authenticate(ctx context.Context, email string, password string, orgID valuer.UUID) (*authtypes.Identity, error) {
|
||||
user, factorPassword, _, err := a.store.GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx, email, orgID)
|
||||
user, factorPassword, userRoles, err := a.store.GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx, email, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -30,5 +30,11 @@ func (a *AuthN) Authenticate(ctx context.Context, email string, password string,
|
||||
return nil, errors.New(errors.TypeUnauthenticated, types.ErrCodeIncorrectPassword, "invalid email or password")
|
||||
}
|
||||
|
||||
return authtypes.NewPrincipalUserIdentity(user.ID, orgID, user.Email, authtypes.IdentNProviderTokenizer), nil
|
||||
if len(userRoles) == 0 {
|
||||
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
|
||||
}
|
||||
|
||||
role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name]
|
||||
|
||||
return authtypes.NewIdentity(user.ID, orgID, user.Email, role, authtypes.IdentNProviderTokenizer), nil
|
||||
}
|
||||
|
||||
@@ -97,7 +97,11 @@ 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,
|
||||
authtypes.ErrCodeRoleNotFound,
|
||||
"not all roles found for the provided names: %v", names,
|
||||
)
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
@@ -118,7 +122,11 @@ 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 names: %v", ids)
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(
|
||||
nil,
|
||||
authtypes.ErrCodeRoleNotFound,
|
||||
"not all roles found for the provided ids: %v", ids,
|
||||
)
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
|
||||
@@ -148,12 +148,7 @@ func (provider *provider) Grant(ctx context.Context, orgID valuer.UUID, names []
|
||||
return err
|
||||
}
|
||||
|
||||
err = provider.Write(ctx, tuples, nil)
|
||||
if err != nil {
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "failed to grant roles: %v to subject: %s", names, subject)
|
||||
}
|
||||
|
||||
return nil
|
||||
return provider.Write(ctx, tuples, nil)
|
||||
}
|
||||
|
||||
func (provider *provider) ModifyGrant(ctx context.Context, orgID valuer.UUID, existingRoleNames []string, updatedRoleNames []string, subject string) error {
|
||||
@@ -185,13 +180,7 @@ func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, names [
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = provider.Write(ctx, nil, tuples)
|
||||
if err != nil {
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "failed to revoke roles: %v to subject: %s", names, subject)
|
||||
}
|
||||
|
||||
return nil
|
||||
return provider.Write(ctx, nil, tuples)
|
||||
}
|
||||
|
||||
func (provider *provider) CreateManagedRoles(ctx context.Context, _ valuer.UUID, managedRoles []*authtypes.Role) error {
|
||||
|
||||
@@ -140,22 +140,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:
|
||||
user, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subject = user
|
||||
case authtypes.PrincipalServiceAccount:
|
||||
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)
|
||||
|
||||
@@ -20,12 +20,6 @@ var (
|
||||
CodeLicenseUnavailable = Code{"license_unavailable"}
|
||||
)
|
||||
|
||||
var (
|
||||
// Used when reverse engineering an error from a response that doesn't have a code.
|
||||
// This should never be used in the codebase, and if it is, it's a bug that should be fixed by using proper error handling and including error codes in responses.
|
||||
CodeUnset = Code{"unset"}
|
||||
)
|
||||
|
||||
var (
|
||||
codeRegex = regexp.MustCompile(`^[a-z_]+$`)
|
||||
)
|
||||
|
||||
@@ -15,16 +15,14 @@ type ServeOpenAPIFunc func(openapi.OperationContext)
|
||||
type Handler interface {
|
||||
http.Handler
|
||||
ServeOpenAPI(openapi.OperationContext)
|
||||
AuditDef() *AuditDef
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
handlerFunc http.HandlerFunc
|
||||
openAPIDef OpenAPIDef
|
||||
auditDef *AuditDef
|
||||
}
|
||||
|
||||
func New(handlerFunc http.HandlerFunc, openAPIDef OpenAPIDef, opts ...Option) Handler {
|
||||
func New(handlerFunc http.HandlerFunc, openAPIDef OpenAPIDef) Handler {
|
||||
// Remove duplicate error status codes
|
||||
openAPIDef.ErrorStatusCodes = slices.DeleteFunc(openAPIDef.ErrorStatusCodes, func(statusCode int) bool {
|
||||
return statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden || statusCode == http.StatusInternalServerError
|
||||
@@ -38,16 +36,10 @@ func New(handlerFunc http.HandlerFunc, openAPIDef OpenAPIDef, opts ...Option) Ha
|
||||
openAPIDef.ErrorStatusCodes = append(openAPIDef.ErrorStatusCodes, http.StatusUnauthorized, http.StatusForbidden)
|
||||
}
|
||||
|
||||
handler := &handler{
|
||||
return &handler{
|
||||
handlerFunc: handlerFunc,
|
||||
openAPIDef: openAPIDef,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(handler)
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
func (handler *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
@@ -128,8 +120,5 @@ func (handler *handler) ServeOpenAPI(opCtx openapi.OperationContext) {
|
||||
openapi.WithHTTPStatus(statusCode),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *handler) AuditDef() *AuditDef {
|
||||
return handler.auditDef
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
)
|
||||
|
||||
// Option configures optional behaviour on a handler created by New.
|
||||
type Option func(*handler)
|
||||
|
||||
type AuditDef struct {
|
||||
ResourceName string // AuthZ Typeable.Name() value, e.g. "dashboard", "user".
|
||||
Action audittypes.Action // create, update, delete, login, etc.
|
||||
Category audittypes.ActionCategory // access_control, configuration_change, etc.
|
||||
ResourceIDParam string // Gorilla mux path param name for the resource ID.
|
||||
}
|
||||
|
||||
// WithAudit attaches an AuditDef to the handler. The actual audit event
|
||||
// emission is handled by the middleware layer, which reads the AuditDef
|
||||
// from the matched route's handler.
|
||||
func WithAuditDef(def AuditDef) Option {
|
||||
return func(h *handler) {
|
||||
h.auditDef = &def
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/auditor"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/types/audittypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
)
|
||||
|
||||
const (
|
||||
logMessage = "::RECEIVED-REQUEST::"
|
||||
)
|
||||
|
||||
type Audit struct {
|
||||
logger *slog.Logger
|
||||
excludedRoutes map[string]struct{}
|
||||
auditor auditor.Auditor
|
||||
}
|
||||
|
||||
func NewAudit(logger *slog.Logger, excludedRoutes []string, auditor auditor.Auditor) *Audit {
|
||||
excludedRoutesMap := make(map[string]struct{})
|
||||
for _, route := range excludedRoutes {
|
||||
excludedRoutesMap[route] = struct{}{}
|
||||
}
|
||||
|
||||
return &Audit{
|
||||
logger: logger.With(slog.String("pkg", pkgname)),
|
||||
excludedRoutes: excludedRoutesMap,
|
||||
auditor: auditor,
|
||||
}
|
||||
}
|
||||
|
||||
func (middleware *Audit) Wrap(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
start := time.Now()
|
||||
host, port, _ := net.SplitHostPort(req.Host)
|
||||
path, err := mux.CurrentRoute(req).GetPathTemplate()
|
||||
if err != nil {
|
||||
path = req.URL.Path
|
||||
}
|
||||
|
||||
fields := []any{
|
||||
string(semconv.ClientAddressKey), req.RemoteAddr,
|
||||
string(semconv.UserAgentOriginalKey), req.UserAgent(),
|
||||
string(semconv.ServerAddressKey), host,
|
||||
string(semconv.ServerPortKey), port,
|
||||
string(semconv.HTTPRequestSizeKey), req.ContentLength,
|
||||
string(semconv.HTTPRouteKey), path,
|
||||
}
|
||||
|
||||
responseBuffer := &byteBuffer{}
|
||||
writer := newResponseCapture(rw, responseBuffer)
|
||||
next.ServeHTTP(writer, req)
|
||||
|
||||
statusCode, writeErr := writer.StatusCode(), writer.WriteError()
|
||||
|
||||
// Logging or Audit: skip if the matched route is in the excluded list. This allows us to exclude noisy routes (e.g. health checks) from both logging and audit.
|
||||
if _, ok := middleware.excludedRoutes[path]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
middleware.emitAuditEvent(req, writer, path)
|
||||
|
||||
fields = append(fields,
|
||||
string(semconv.HTTPResponseStatusCodeKey), statusCode,
|
||||
string(semconv.HTTPServerRequestDurationName), time.Since(start),
|
||||
)
|
||||
if writeErr != nil {
|
||||
fields = append(fields, errors.Attr(writeErr))
|
||||
middleware.logger.ErrorContext(req.Context(), logMessage, fields...)
|
||||
} else {
|
||||
if responseBuffer.Len() != 0 {
|
||||
fields = append(fields, "response.body", responseBuffer.String())
|
||||
}
|
||||
|
||||
middleware.logger.InfoContext(req.Context(), logMessage, fields...)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCapture, routeTemplate string) {
|
||||
if middleware.auditor == nil {
|
||||
return
|
||||
}
|
||||
|
||||
def := auditDefFromRequest(req)
|
||||
if def == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// extract claims
|
||||
claims, _ := authtypes.ClaimsFromContext(req.Context())
|
||||
|
||||
// extract status code
|
||||
statusCode := writer.StatusCode()
|
||||
|
||||
// extract traces.
|
||||
span := trace.SpanFromContext(req.Context())
|
||||
|
||||
// extract error details.
|
||||
var errorType, errorCode string
|
||||
if statusCode >= 400 {
|
||||
errorType = render.ErrorTypeFromStatusCode(statusCode)
|
||||
errorCode = render.ErrorCodeFromBody(writer.BodyBytes())
|
||||
}
|
||||
|
||||
event := audittypes.NewAuditEventFromHTTPRequest(
|
||||
req,
|
||||
routeTemplate,
|
||||
statusCode,
|
||||
span.SpanContext().TraceID(),
|
||||
span.SpanContext().SpanID(),
|
||||
def.Action,
|
||||
def.Category,
|
||||
claims,
|
||||
resourceIDFromRequest(req, def.ResourceIDParam),
|
||||
def.ResourceName,
|
||||
errorType,
|
||||
errorCode,
|
||||
)
|
||||
|
||||
middleware.auditor.Audit(req.Context(), event)
|
||||
}
|
||||
|
||||
func auditDefFromRequest(req *http.Request) *handler.AuditDef {
|
||||
route := mux.CurrentRoute(req)
|
||||
if route == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
actualHandler := route.GetHandler()
|
||||
if actualHandler == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// The type assertion is necessary because route.GetHandler() returns
|
||||
// http.Handler, and not every http.Handler on the mux is a handler.Handler
|
||||
// (e.g. middleware wrappers, raw http.HandlerFunc registrations).
|
||||
provider, ok := actualHandler.(handler.Handler)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return provider.AuditDef()
|
||||
}
|
||||
|
||||
func resourceIDFromRequest(req *http.Request, param string) string {
|
||||
if param == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
vars := mux.Vars(req)
|
||||
if vars == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return vars[param]
|
||||
}
|
||||
@@ -40,6 +40,17 @@ func (middleware *AuthZ) ViewAccess(next http.HandlerFunc) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if claims.IdentNProvider == authtypes.IdentNProviderAPIKey.StringValue() {
|
||||
if err := claims.IsViewer(); err != nil {
|
||||
middleware.logger.WarnContext(ctx, authzDeniedMessage, slog.Any("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),
|
||||
@@ -79,6 +90,17 @@ func (middleware *AuthZ) EditAccess(next http.HandlerFunc) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if claims.IdentNProvider == authtypes.IdentNProviderAPIKey.StringValue() {
|
||||
if err := claims.IsEditor(); err != nil {
|
||||
middleware.logger.WarnContext(ctx, authzDeniedMessage, slog.Any("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),
|
||||
@@ -117,6 +139,17 @@ func (middleware *AuthZ) AdminAccess(next http.HandlerFunc) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if claims.IdentNProvider == authtypes.IdentNProviderAPIKey.StringValue() {
|
||||
if err := claims.IsAdmin(); err != nil {
|
||||
middleware.logger.WarnContext(ctx, authzDeniedMessage, slog.Any("claims", claims))
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
next(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
selectors := []authtypes.Selector{
|
||||
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
|
||||
}
|
||||
@@ -153,28 +186,13 @@ func (middleware *AuthZ) SelfAccess(next http.HandlerFunc) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
selectors := []authtypes.Selector{
|
||||
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
|
||||
id := mux.Vars(req)["id"]
|
||||
if err := claims.IsSelfAccess(id); err != nil {
|
||||
middleware.logger.WarnContext(req.Context(), authzDeniedMessage, slog.Any("claims", claims))
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = middleware.authzService.CheckWithTupleCreation(
|
||||
req.Context(),
|
||||
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, slog.Any("claims", claims))
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
next(rw, req)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,10 +61,8 @@ func (m *IdentN) Wrap(next http.Handler) http.Handler {
|
||||
ctx = authtypes.NewContextWithClaims(ctx, claims)
|
||||
|
||||
comment := ctxtypes.CommentFromContext(ctx)
|
||||
comment.Set("identn_provider", claims.IdentNProvider.StringValue())
|
||||
comment.Set("identn_provider", claims.IdentNProvider)
|
||||
comment.Set("user_id", claims.UserID)
|
||||
comment.Set("service_account_id", claims.ServiceAccountID)
|
||||
comment.Set("principal", claims.Principal.StringValue())
|
||||
comment.Set("org_id", claims.OrgID)
|
||||
ctx = ctxtypes.NewContextWithComment(ctx, comment)
|
||||
|
||||
|
||||
81
pkg/http/middleware/logging.go
Normal file
81
pkg/http/middleware/logging.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
logMessage string = "::RECEIVED-REQUEST::"
|
||||
)
|
||||
|
||||
type Logging struct {
|
||||
logger *slog.Logger
|
||||
excludedRoutes map[string]struct{}
|
||||
}
|
||||
|
||||
func NewLogging(logger *slog.Logger, excludedRoutes []string) *Logging {
|
||||
excludedRoutesMap := make(map[string]struct{})
|
||||
for _, route := range excludedRoutes {
|
||||
excludedRoutesMap[route] = struct{}{}
|
||||
}
|
||||
|
||||
return &Logging{
|
||||
logger: logger.With(slog.String("pkg", pkgname)),
|
||||
excludedRoutes: excludedRoutesMap,
|
||||
}
|
||||
}
|
||||
|
||||
func (middleware *Logging) Wrap(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
start := time.Now()
|
||||
host, port, _ := net.SplitHostPort(req.Host)
|
||||
path, err := mux.CurrentRoute(req).GetPathTemplate()
|
||||
if err != nil {
|
||||
path = req.URL.Path
|
||||
}
|
||||
|
||||
fields := []any{
|
||||
string(semconv.ClientAddressKey), req.RemoteAddr,
|
||||
string(semconv.UserAgentOriginalKey), req.UserAgent(),
|
||||
string(semconv.ServerAddressKey), host,
|
||||
string(semconv.ServerPortKey), port,
|
||||
string(semconv.HTTPRequestSizeKey), req.ContentLength,
|
||||
string(semconv.HTTPRouteKey), path,
|
||||
}
|
||||
|
||||
badResponseBuffer := new(bytes.Buffer)
|
||||
writer := newBadResponseLoggingWriter(rw, badResponseBuffer)
|
||||
next.ServeHTTP(writer, req)
|
||||
|
||||
// if the path is in the excludedRoutes map, don't log
|
||||
if _, ok := middleware.excludedRoutes[path]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
statusCode, err := writer.StatusCode(), writer.WriteError()
|
||||
fields = append(fields,
|
||||
string(semconv.HTTPResponseStatusCodeKey), statusCode,
|
||||
string(semconv.HTTPServerRequestDurationName), time.Since(start),
|
||||
)
|
||||
if err != nil {
|
||||
fields = append(fields, errors.Attr(err))
|
||||
middleware.logger.ErrorContext(req.Context(), logMessage, fields...)
|
||||
} else {
|
||||
// when the status code is 400 or >=500, and the response body is not empty.
|
||||
if badResponseBuffer.Len() != 0 {
|
||||
fields = append(fields, "response.body", badResponseBuffer.String())
|
||||
}
|
||||
|
||||
middleware.logger.InfoContext(req.Context(), logMessage, fields...)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package middleware
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
@@ -9,156 +10,118 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
maxResponseBodyCapture int = 4096 // At most 4k bytes from response bodies.
|
||||
maxResponseBodyInLogs = 4096 // At most 4k bytes from response bodies in our logs.
|
||||
)
|
||||
|
||||
// Wraps an http.ResponseWriter to capture the status code,
|
||||
// write errors, and (for error responses) a bounded slice of the body.
|
||||
type responseCapture interface {
|
||||
type badResponseLoggingWriter interface {
|
||||
http.ResponseWriter
|
||||
|
||||
// StatusCode returns the HTTP status code written to the response.
|
||||
// Get the status code.
|
||||
StatusCode() int
|
||||
|
||||
// WriteError returns the error (if any) from the downstream Write call.
|
||||
// Get the error while writing.
|
||||
WriteError() error
|
||||
|
||||
// BodyBytes returns the captured response body bytes. Only populated
|
||||
// for error responses (status >= 400).
|
||||
BodyBytes() []byte
|
||||
}
|
||||
|
||||
func newResponseCapture(rw http.ResponseWriter, buffer *byteBuffer) responseCapture {
|
||||
b := nonFlushingResponseCapture{
|
||||
func newBadResponseLoggingWriter(rw http.ResponseWriter, buffer io.Writer) badResponseLoggingWriter {
|
||||
b := nonFlushingBadResponseLoggingWriter{
|
||||
rw: rw,
|
||||
buffer: buffer,
|
||||
captureBody: false,
|
||||
bodyBytesLeft: maxResponseBodyCapture,
|
||||
logBody: false,
|
||||
bodyBytesLeft: maxResponseBodyInLogs,
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
if f, ok := rw.(http.Flusher); ok {
|
||||
return &flushingResponseCapture{nonFlushingResponseCapture: b, f: f}
|
||||
return &flushingBadResponseLoggingWriter{b, f}
|
||||
}
|
||||
|
||||
return &b
|
||||
}
|
||||
|
||||
// byteBuffer is a minimal write-only buffer used to capture response bodies.
|
||||
type byteBuffer struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (b *byteBuffer) Write(p []byte) (int, error) {
|
||||
b.buf = append(b.buf, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (b *byteBuffer) WriteString(s string) (int, error) {
|
||||
b.buf = append(b.buf, s...)
|
||||
return len(s), nil
|
||||
}
|
||||
|
||||
func (b *byteBuffer) Bytes() []byte {
|
||||
return b.buf
|
||||
}
|
||||
|
||||
func (b *byteBuffer) Len() int {
|
||||
return len(b.buf)
|
||||
}
|
||||
|
||||
func (b *byteBuffer) String() string {
|
||||
return string(b.buf)
|
||||
}
|
||||
|
||||
type nonFlushingResponseCapture struct {
|
||||
type nonFlushingBadResponseLoggingWriter struct {
|
||||
rw http.ResponseWriter
|
||||
buffer *byteBuffer
|
||||
captureBody bool
|
||||
buffer io.Writer
|
||||
logBody bool
|
||||
bodyBytesLeft int
|
||||
statusCode int
|
||||
writeError error
|
||||
writeError error // The error returned when downstream Write() fails.
|
||||
}
|
||||
|
||||
type flushingResponseCapture struct {
|
||||
nonFlushingResponseCapture
|
||||
// Extends nonFlushingBadResponseLoggingWriter that implements http.Flusher.
|
||||
type flushingBadResponseLoggingWriter struct {
|
||||
nonFlushingBadResponseLoggingWriter
|
||||
f http.Flusher
|
||||
}
|
||||
|
||||
// Unwrap is used by http.ResponseController to get access to original http.ResponseWriter.
|
||||
func (writer *nonFlushingResponseCapture) Unwrap() http.ResponseWriter {
|
||||
// Unwrap method is used by http.ResponseController to get access to original http.ResponseWriter.
|
||||
func (writer *nonFlushingBadResponseLoggingWriter) Unwrap() http.ResponseWriter {
|
||||
return writer.rw
|
||||
}
|
||||
|
||||
// Header returns the header map that will be sent by WriteHeader.
|
||||
func (writer *nonFlushingResponseCapture) Header() http.Header {
|
||||
// Implements ResponseWriter.
|
||||
func (writer *nonFlushingBadResponseLoggingWriter) Header() http.Header {
|
||||
return writer.rw.Header()
|
||||
}
|
||||
|
||||
// WriteHeader writes the HTTP response header.
|
||||
func (writer *nonFlushingResponseCapture) WriteHeader(statusCode int) {
|
||||
func (writer *nonFlushingBadResponseLoggingWriter) WriteHeader(statusCode int) {
|
||||
writer.statusCode = statusCode
|
||||
if statusCode >= 400 {
|
||||
writer.captureBody = true
|
||||
if statusCode >= 500 || statusCode == 400 {
|
||||
writer.logBody = true
|
||||
}
|
||||
|
||||
writer.rw.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
// Write writes HTTP response data.
|
||||
func (writer *nonFlushingResponseCapture) Write(data []byte) (int, error) {
|
||||
// Writes HTTP response data.
|
||||
func (writer *nonFlushingBadResponseLoggingWriter) Write(data []byte) (int, error) {
|
||||
if writer.statusCode == 0 {
|
||||
// WriteHeader has (probably) not been called, so we need to call it with StatusOK to fulfill the interface contract.
|
||||
// https://godoc.org/net/http#ResponseWriter
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// 204 No Content is a success response that indicates that the request has been successfully processed and that the response body is intentionally empty.
|
||||
if writer.statusCode == 204 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
n, err := writer.rw.Write(data)
|
||||
if writer.captureBody {
|
||||
if writer.logBody {
|
||||
writer.captureResponseBody(data)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
writer.writeError = err
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Hijack hijacks the first response writer that is a Hijacker.
|
||||
func (writer *nonFlushingResponseCapture) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
func (writer *nonFlushingBadResponseLoggingWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
hj, ok := writer.rw.(http.Hijacker)
|
||||
if ok {
|
||||
return hj.Hijack()
|
||||
}
|
||||
|
||||
return nil, nil, errors.NewInternalf(errors.CodeInternal, "cannot cast underlying response writer to Hijacker")
|
||||
}
|
||||
|
||||
func (writer *nonFlushingResponseCapture) StatusCode() int {
|
||||
func (writer *nonFlushingBadResponseLoggingWriter) StatusCode() int {
|
||||
return writer.statusCode
|
||||
}
|
||||
|
||||
func (writer *nonFlushingResponseCapture) WriteError() error {
|
||||
func (writer *nonFlushingBadResponseLoggingWriter) WriteError() error {
|
||||
return writer.writeError
|
||||
}
|
||||
|
||||
func (writer *nonFlushingResponseCapture) BodyBytes() []byte {
|
||||
return writer.buffer.Bytes()
|
||||
}
|
||||
|
||||
func (writer *flushingResponseCapture) Flush() {
|
||||
func (writer *flushingBadResponseLoggingWriter) Flush() {
|
||||
writer.f.Flush()
|
||||
}
|
||||
|
||||
func (writer *nonFlushingResponseCapture) captureResponseBody(data []byte) {
|
||||
func (writer *nonFlushingBadResponseLoggingWriter) captureResponseBody(data []byte) {
|
||||
if len(data) > writer.bodyBytesLeft {
|
||||
_, _ = writer.buffer.Write(data[:writer.bodyBytesLeft])
|
||||
_, _ = writer.buffer.WriteString("...")
|
||||
_, _ = io.WriteString(writer.buffer, "...")
|
||||
writer.bodyBytesLeft = 0
|
||||
writer.captureBody = false
|
||||
writer.logBody = false
|
||||
} else {
|
||||
_, _ = writer.buffer.Write(data)
|
||||
writer.bodyBytesLeft -= len(data)
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestResponseCapture(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
handler http.HandlerFunc
|
||||
expectedStatus int
|
||||
expectedBodyBytes string
|
||||
expectedClientBody string
|
||||
}{
|
||||
{
|
||||
name: "Success_DoesNotCaptureBody",
|
||||
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_, _ = rw.Write([]byte(`{"status":"success","data":{"id":"123"}}`))
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBodyBytes: "",
|
||||
expectedClientBody: `{"status":"success","data":{"id":"123"}}`,
|
||||
},
|
||||
{
|
||||
name: "Error_CapturesBody",
|
||||
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.WriteHeader(http.StatusForbidden)
|
||||
_, _ = rw.Write([]byte(`{"status":"error","error":{"code":"authz_forbidden","message":"forbidden"}}`))
|
||||
},
|
||||
expectedStatus: http.StatusForbidden,
|
||||
expectedBodyBytes: `{"status":"error","error":{"code":"authz_forbidden","message":"forbidden"}}`,
|
||||
expectedClientBody: `{"status":"error","error":{"code":"authz_forbidden","message":"forbidden"}}`,
|
||||
},
|
||||
{
|
||||
name: "Error_TruncatesAtMaxCapture",
|
||||
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = rw.Write([]byte(strings.Repeat("x", maxResponseBodyCapture+100)))
|
||||
},
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedBodyBytes: strings.Repeat("x", maxResponseBodyCapture) + "...",
|
||||
expectedClientBody: strings.Repeat("x", maxResponseBodyCapture+100),
|
||||
},
|
||||
{
|
||||
name: "NoContent_SuppressesWrite",
|
||||
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
_, _ = rw.Write([]byte("should be suppressed"))
|
||||
},
|
||||
expectedStatus: http.StatusNoContent,
|
||||
expectedBodyBytes: "",
|
||||
expectedClientBody: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var captured responseCapture
|
||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
buf := &byteBuffer{}
|
||||
captured = newResponseCapture(rw, buf)
|
||||
testCase.handler(captured, req)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
resp, err := http.Get(server.URL)
|
||||
assert.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
clientBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Equal(t, testCase.expectedStatus, captured.StatusCode())
|
||||
assert.Equal(t, testCase.expectedBodyBytes, string(captured.BodyBytes()))
|
||||
assert.Equal(t, testCase.expectedClientBody, string(clientBody))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -43,45 +42,6 @@ func Success(rw http.ResponseWriter, httpCode int, data interface{}) {
|
||||
_, _ = rw.Write(body)
|
||||
}
|
||||
|
||||
func ErrorCodeFromBody(body []byte) string {
|
||||
code := gjson.GetBytes(body, "error.code").String()
|
||||
|
||||
// This should never return empty since we only call this function on responses that were generated by us.
|
||||
// If it does return empty, the codebase has failed to use render package for error responses somewhere, and we should fix that instead of trying to handle it here.
|
||||
if code == "" {
|
||||
return errors.CodeUnset.String()
|
||||
}
|
||||
|
||||
return code
|
||||
}
|
||||
|
||||
func ErrorTypeFromStatusCode(statusCode int) string {
|
||||
// We are losing the exact type information here, but we can at least capture the error code and message for better observability.
|
||||
// To get the exact type, we would need some changes in the render package to include the error type in the response, which we can consider in the future if there is a need for it.
|
||||
switch statusCode {
|
||||
case http.StatusBadRequest:
|
||||
return errors.TypeInvalidInput.String()
|
||||
case http.StatusNotFound:
|
||||
return errors.TypeNotFound.String()
|
||||
case http.StatusConflict:
|
||||
return errors.TypeAlreadyExists.String()
|
||||
case http.StatusUnauthorized:
|
||||
return errors.TypeUnauthenticated.String()
|
||||
case http.StatusNotImplemented:
|
||||
return errors.TypeUnsupported.String()
|
||||
case http.StatusForbidden:
|
||||
return errors.TypeForbidden.String()
|
||||
case statusClientClosedConnection:
|
||||
return errors.TypeCanceled.String()
|
||||
case http.StatusGatewayTimeout:
|
||||
return errors.TypeTimeout.String()
|
||||
case http.StatusUnavailableForLegalReasons:
|
||||
return errors.TypeLicenseUnavailable.String()
|
||||
default:
|
||||
return errors.TypeInternal.String()
|
||||
}
|
||||
}
|
||||
|
||||
func Error(rw http.ResponseWriter, cause error) {
|
||||
// Derive the http code from the error type
|
||||
t, _, _, _, _, _ := errors.Unwrapb(cause)
|
||||
|
||||
@@ -58,31 +58,6 @@ func TestSuccess(t *testing.T) {
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestErrorCodeFromBody(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
body []byte
|
||||
wantCode string
|
||||
}{
|
||||
{
|
||||
name: "ValidErrorResponse",
|
||||
body: []byte(`{"status":"error","error":{"code":"authz_forbidden","message":"only admins can access this resource"}}`),
|
||||
wantCode: "authz_forbidden",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON",
|
||||
body: []byte(`not json`),
|
||||
wantCode: "unset",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.wantCode, ErrorCodeFromBody(testCase.body))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "localhost:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -10,29 +10,33 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/identn"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
)
|
||||
|
||||
// todo: will move this in types layer with service account integration
|
||||
type apiKeyTokenKey struct{}
|
||||
|
||||
type provider struct {
|
||||
serviceAccount serviceaccount.Module
|
||||
config identn.Config
|
||||
settings factory.ScopedProviderSettings
|
||||
sfGroup *singleflight.Group
|
||||
store sqlstore.SQLStore
|
||||
config identn.Config
|
||||
settings factory.ScopedProviderSettings
|
||||
sfGroup *singleflight.Group
|
||||
}
|
||||
|
||||
func NewFactory(serviceAccount serviceaccount.Module) factory.ProviderFactory[identn.IdentN, identn.Config] {
|
||||
func NewFactory(store sqlstore.SQLStore) factory.ProviderFactory[identn.IdentN, identn.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName(authtypes.IdentNProviderAPIKey.StringValue()), func(ctx context.Context, providerSettings factory.ProviderSettings, config identn.Config) (identn.IdentN, error) {
|
||||
return New(serviceAccount, config, providerSettings)
|
||||
return New(providerSettings, store, config)
|
||||
})
|
||||
}
|
||||
|
||||
func New(serviceAccount serviceaccount.Module, config identn.Config, providerSettings factory.ProviderSettings) (identn.IdentN, error) {
|
||||
func New(providerSettings factory.ProviderSettings, store sqlstore.SQLStore, config identn.Config) (identn.IdentN, error) {
|
||||
return &provider{
|
||||
serviceAccount: serviceAccount,
|
||||
config: config,
|
||||
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/identn/apikeyidentn"),
|
||||
sfGroup: &singleflight.Group{},
|
||||
store: store,
|
||||
config: config,
|
||||
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/identn/apikeyidentn"),
|
||||
sfGroup: &singleflight.Group{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -50,44 +54,75 @@ func (provider *provider) Test(req *http.Request) bool {
|
||||
}
|
||||
|
||||
func (provider *provider) Pre(req *http.Request) *http.Request {
|
||||
apiKey := provider.extractToken(req)
|
||||
if apiKey == "" {
|
||||
token := provider.extractToken(req)
|
||||
if token == "" {
|
||||
return req
|
||||
}
|
||||
|
||||
ctx := authtypes.NewContextWithAPIKey(req.Context(), apiKey)
|
||||
ctx := context.WithValue(req.Context(), apiKeyTokenKey{}, token)
|
||||
return req.WithContext(ctx)
|
||||
}
|
||||
|
||||
func (provider *provider) GetIdentity(req *http.Request) (*authtypes.Identity, error) {
|
||||
ctx := req.Context()
|
||||
apiKey, err := authtypes.APIKeyFromContext(ctx)
|
||||
apiKeyToken, ok := ctx.Value(apiKeyTokenKey{}).(string)
|
||||
if !ok || apiKeyToken == "" {
|
||||
return nil, errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "missing api key")
|
||||
}
|
||||
|
||||
var apiKey types.StorableAPIKey
|
||||
err := provider.
|
||||
store.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(&apiKey).
|
||||
Where("token = ?", apiKeyToken).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
identity, err := provider.serviceAccount.GetIdentity(ctx, apiKey)
|
||||
if apiKey.ExpiresAt.Before(time.Now()) && !apiKey.ExpiresAt.Equal(types.NEVER_EXPIRES) {
|
||||
return nil, errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "api key has expired")
|
||||
}
|
||||
|
||||
var user types.User
|
||||
err = provider.
|
||||
store.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(&user).
|
||||
Where("id = ?", apiKey.UserID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
identity := authtypes.NewIdentity(user.ID, user.OrgID, user.Email, apiKey.Role, provider.Name())
|
||||
return identity, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Post(ctx context.Context, _ *http.Request, _ authtypes.Claims) {
|
||||
apiKey, err := authtypes.APIKeyFromContext(ctx)
|
||||
if err != nil {
|
||||
apiKeyToken, ok := ctx.Value(apiKeyTokenKey{}).(string)
|
||||
if !ok || apiKeyToken == "" {
|
||||
return
|
||||
}
|
||||
|
||||
_, _, _ = provider.sfGroup.Do(apiKey, func() (any, error) {
|
||||
if err := provider.serviceAccount.SetLastObservedAt(ctx, apiKey, time.Now()); err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "failed to set last observed at", errors.Attr(err))
|
||||
return false, err
|
||||
_, _, _ = provider.sfGroup.Do(apiKeyToken, func() (any, error) {
|
||||
_, err := provider.
|
||||
store.
|
||||
BunDB().
|
||||
NewUpdate().
|
||||
Model(new(types.StorableAPIKey)).
|
||||
Set("last_used = ?", time.Now()).
|
||||
Where("token = ?", apiKeyToken).
|
||||
Where("revoked = false").
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "failed to update last used of api key", errors.Attr(err))
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (provider *provider) extractToken(req *http.Request) string {
|
||||
|
||||
@@ -79,15 +79,22 @@ func (provider *provider) GetIdentity(req *http.Request) (*authtypes.Identity, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rootUser, _, err := provider.userGetter.GetRootUserByOrgID(ctx, org.ID)
|
||||
rootUser, userRoles, err := provider.userGetter.GetRootUserByOrgID(ctx, org.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider.identity = authtypes.NewPrincipalUserIdentity(
|
||||
if len(userRoles) == 0 {
|
||||
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
|
||||
}
|
||||
|
||||
role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name]
|
||||
|
||||
provider.identity = authtypes.NewIdentity(
|
||||
rootUser.ID,
|
||||
rootUser.OrgID,
|
||||
rootUser.Email,
|
||||
role,
|
||||
authtypes.IdentNProviderImpersonation,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package implcloudintegration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
citypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
)
|
||||
|
||||
type definitionStore struct{}
|
||||
|
||||
func NewDefinitionStore() citypes.ServiceDefinitionStore {
|
||||
return &definitionStore{}
|
||||
}
|
||||
|
||||
func (d *definitionStore) Get(ctx context.Context, provider citypes.CloudProviderType, serviceID citypes.ServiceID) (*citypes.ServiceDefinition, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (d *definitionStore) List(ctx context.Context, provider citypes.CloudProviderType) ([]*citypes.ServiceDefinition, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 85 85" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x="2.5" y="2.5"/><symbol id="A" overflow="visible"><g stroke="none"><path d="M0 41.579C0 20.293 17.84 3.157 40 3.157s40 17.136 40 38.422S62.16 80 40 80 0 62.864 0 41.579z" fill="#9d5025"/><path d="M0 38.422C0 17.136 17.84 0 40 0s40 17.136 40 38.422-17.84 38.422-40 38.422S0 59.707 0 38.422z" fill="#f58536"/><path d="M51.672 7.387v13.952H28.327V7.387zm18.061 40.378v11.364h-11.83V47.765zm-14.958 0v11.364h-11.83V47.765zm-18.206 0v11.364h-11.83V47.765zm-14.959 0v11.364H9.78V47.765z"/><path d="M14.63 37.929h2.13v11.149h-2.13z"/><path d="M14.63 37.929h17.088v2.045H14.63z"/><path d="M29.589 37.929h2.13v11.149H29.59zm18.206 0h2.13v11.149h-2.13z"/><path d="M47.795 37.929h17.088v2.045H47.795z"/><path d="M62.754 37.929h2.13v11.149h-2.129zm-40.631-7.954h2.13v8.977h-2.13zM38.935 19.28h2.13v10.859h-2.129z"/><path d="M22.123 29.116h35.32v2.045h-35.32z"/><path d="M55.314 29.975h2.13v8.977h-2.129z"/></g></symbol></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,468 +0,0 @@
|
||||
{
|
||||
"id": "alb",
|
||||
"title": "ALB",
|
||||
"icon": "file://icon.svg",
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": false
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
{
|
||||
"name": "aws_ApplicationELB_ActiveConnectionCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ActiveConnectionCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ActiveConnectionCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ActiveConnectionCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_AnomalousHostCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_AnomalousHostCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_AnomalousHostCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_AnomalousHostCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ConsumedLCUs_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ConsumedLCUs_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ConsumedLCUs_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ConsumedLCUs_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HTTPCode_Target_2XX_Count_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HTTPCode_Target_2XX_Count_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HTTPCode_Target_2XX_Count_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HTTPCode_Target_2XX_Count_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HTTPCode_Target_4XX_Count_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HTTPCode_Target_4XX_Count_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HTTPCode_Target_4XX_Count_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HTTPCode_Target_4XX_Count_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyHostCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyHostCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyHostCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyHostCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyStateDNS_count",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyStateDNS_max",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyStateDNS_min",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyStateDNS_sum",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyStateRouting_count",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyStateRouting_max",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyStateRouting_min",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_HealthyStateRouting_sum",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_MitigatedHostCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_MitigatedHostCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_MitigatedHostCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_MitigatedHostCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_NewConnectionCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_NewConnectionCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_NewConnectionCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_NewConnectionCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_PeakLCUs_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_PeakLCUs_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_PeakLCUs_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_PeakLCUs_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ProcessedBytes_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ProcessedBytes_max",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ProcessedBytes_min",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_ProcessedBytes_sum",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_RequestCountPerTarget_count",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_RequestCountPerTarget_max",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_RequestCountPerTarget_min",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_RequestCountPerTarget_sum",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_RequestCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_RequestCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_RequestCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_RequestCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_TargetResponseTime_count",
|
||||
"unit": "Seconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_TargetResponseTime_max",
|
||||
"unit": "Seconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_TargetResponseTime_min",
|
||||
"unit": "Seconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_TargetResponseTime_sum",
|
||||
"unit": "Seconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnHealthyHostCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnHealthyHostCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnHealthyHostCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnHealthyHostCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnhealthyStateDNS_count",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnhealthyStateDNS_max",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnhealthyStateDNS_min",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnhealthyStateDNS_sum",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnhealthyStateRouting_count",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnhealthyStateRouting_max",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnhealthyStateRouting_min",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "aws_ApplicationELB_UnhealthyStateRouting_sum",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"logs": []
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"aws": {
|
||||
"metrics": {
|
||||
"cloudwatchMetricStreamFilters": [
|
||||
{
|
||||
"Namespace": "AWS/ApplicationELB"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
{
|
||||
"id": "overview",
|
||||
"title": "ALB Overview",
|
||||
"description": "Overview of Application Load Balancer",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
### Monitor Application Load Balancers with SigNoz
|
||||
|
||||
Collect key ALB metrics and view them with an out of the box dashboard.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,14 +0,0 @@
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient x1="0%" y1="100%" x2="100%" y2="0%" id="linearGradient-1">
|
||||
<stop stop-color="#4D27A8" offset="0%"></stop>
|
||||
<stop stop-color="#A166FF" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g fill="url(#linearGradient-1)">
|
||||
<rect id="Rectangle" x="0" y="0" width="24" height="24"></rect>
|
||||
</g>
|
||||
<path d="M6,6.76751613 L8,5.43446738 L8,18.5659476 L6,17.2328988 L6,6.76751613 Z M5,6.49950633 L5,17.4999086 C5,17.6669147 5.084,17.8239204 5.223,17.9159238 L8.223,19.9159969 C8.307,19.971999 8.403,20 8.5,20 C8.581,20 8.662,19.9809993 8.736,19.9409978 C8.898,19.8539947 9,19.6849885 9,19.4999817 L9,16.9998903 L10,16.9998903 L10,15.9998537 L9,15.9998537 L9,7.99956118 L10,7.99956118 L10,6.99952461 L9,6.99952461 L9,4.49943319 C9,4.31542646 8.898,4.14542025 8.736,4.0594171 C8.574,3.97241392 8.377,3.98141425 8.223,4.08341798 L5.223,6.08349112 C5.084,6.17649452 5,6.33250022 5,6.49950633 L5,6.49950633 Z M19,17.2328988 L17,18.5659476 L17,5.43446738 L19,6.76751613 L19,17.2328988 Z M19.777,6.08349112 L16.777,4.08341798 C16.623,3.98141425 16.426,3.97241392 16.264,4.0594171 C16.102,4.14542025 16,4.31542646 16,4.49943319 L16,6.99952461 L15,6.99952461 L15,7.99956118 L16,7.99956118 L16,15.9998537 L15,15.9998537 L15,16.9998903 L16,16.9998903 L16,19.4999817 C16,19.6849885 16.102,19.8539947 16.264,19.9409978 C16.338,19.9809993 16.419,20 16.5,20 C16.597,20 16.693,19.971999 16.777,19.9159969 L19.777,17.9159238 C19.916,17.8239204 20,17.6669147 20,17.4999086 L20,6.49950633 C20,6.33250022 19.916,6.17649452 19.777,6.08349112 L19.777,6.08349112 Z M13,7.99956118 L14,7.99956118 L14,6.99952461 L13,6.99952461 L13,7.99956118 Z M11,7.99956118 L12,7.99956118 L12,6.99952461 L11,6.99952461 L11,7.99956118 Z M13,16.9998903 L14,16.9998903 L14,15.9998537 L13,15.9998537 L13,16.9998903 Z M11,16.9998903 L12,16.9998903 L12,15.9998537 L11,15.9998537 L11,16.9998903 Z M13.18,14.884813 L10.18,12.3847215 C10.065,12.288718 10,12.1487129 10,11.9997075 C10,11.851702 10.065,11.7106969 10.18,11.6156934 L13.18,9.11560199 L13.82,9.88463011 L11.281,11.9997075 L13.82,14.1157848 L13.18,14.884813 Z" id="Amazon-API-Gateway_Icon_16_Squid" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1,200 +0,0 @@
|
||||
{
|
||||
"id": "api-gateway",
|
||||
"title": "API Gateway",
|
||||
"icon": "file://icon.svg",
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
{
|
||||
"name": "aws_ApiGateway_4XXError_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_4XXError_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_4XXError_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_4XXError_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_5XXError_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_5XXError_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_5XXError_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_5XXError_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_CacheHitCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_CacheHitCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_CacheHitCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_CacheHitCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_CacheMissCount_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_CacheMissCount_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_CacheMissCount_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_CacheMissCount_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_Count_count",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_Count_max",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_Count_min",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_Count_sum",
|
||||
"unit": "Count",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_IntegrationLatency_count",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_IntegrationLatency_max",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_IntegrationLatency_min",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_IntegrationLatency_sum",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_Latency_count",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_Latency_max",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_Latency_min",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge"
|
||||
},
|
||||
{
|
||||
"name": "aws_ApiGateway_Latency_sum",
|
||||
"unit": "Milliseconds",
|
||||
"type": "Gauge"
|
||||
}
|
||||
],
|
||||
"logs": [
|
||||
{
|
||||
"name": "Account Id",
|
||||
"path": "resources.cloud.account.id",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Log Group Name",
|
||||
"path": "resources.aws.cloudwatch.log_group_name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "Log Stream Name",
|
||||
"path": "resources.aws.cloudwatch.log_stream_name",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"aws": {
|
||||
"metrics": {
|
||||
"cloudwatchMetricStreamFilters": [
|
||||
{
|
||||
"Namespace": "AWS/ApiGateway"
|
||||
}
|
||||
]
|
||||
},
|
||||
"logs": {
|
||||
"cloudwatchLogsSubscriptions": [
|
||||
{
|
||||
"logGroupNamePrefix": "API-Gateway",
|
||||
"filterPattern": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
{
|
||||
"id": "overview",
|
||||
"title": "API Gateway Overview",
|
||||
"description": "Overview of API Gateway",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
### Monitor API Gateway with SigNoz
|
||||
|
||||
Collect key API Gateway metrics and view them with an out of the box dashboard.
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user