Compare commits

...

15 Commits

Author SHA1 Message Date
Abhi kumar
c0684dbf79 Merge branch 'fix/issue-6354' into fix/issue-3917 2026-02-23 16:58:50 +05:30
Abhi Kumar
5133346f77 Merge branch 'main' of https://github.com/SigNoz/signoz into fix/issue-6354 2026-02-23 16:58:31 +05:30
Abhi Kumar
d48495aecc fix: fixed tsc 2026-02-23 16:58:14 +05:30
Vikrant Gupta
465e07de83 fix(openapi): make the error and status as mandatory (#10391)
* fix(openapi): make the error and status as mandatory

* fix(openapi): fix the frontend types
2026-02-23 16:47:24 +05:30
Abhi Kumar
67184b4064 fix: thresholds working correctly with number panel 2026-02-23 16:46:36 +05:30
Abhi Kumar
53d7753167 fix: fixed unit converstion support across thresholds and yaxisunit 2026-02-23 16:32:25 +05:30
Vikrant Gupta
c04f664e2f fix(openapi): make the data and status required in success responses (#10390) 2026-02-23 16:15:36 +05:30
Abhi kumar
e406b0bb61 fix: formatting chart manager aggregation values with the yaxis unit (#10379)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix: formatting chart manager aggergation values with the yaxis unit

* chore: pr review changes

* chore: pr review changes
2026-02-23 06:23:26 +00:00
Nikhil Mantri
c143e0b130 chore: show warning and link to doc on missing hostname in infra tab (#10279) 2026-02-23 11:34:29 +05:30
Vikrant Gupta
0dd42ec076 feat(authz): update openapi spec (#10382)
Some checks failed
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
2026-02-20 17:06:33 +00:00
Vikrant Gupta
34ba5bab28 feat(authz): add http routes for authz (#10376)
* feat(authz): add http routes for authz

* feat(authz): update openapi spec

* feat(authz): update openapi spec
2026-02-20 22:12:24 +05:30
Nityananda Gohain
678f015e0b fix: don't send failed if empty rows (#10378)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-02-20 19:30:09 +05:30
Pandey
5a69f16410 refactor: remove legacy /api/gateway reverse proxy (#10374)
## Summary

- Remove the legacy `/api/gateway` reverse proxy from `ee/query-service/integrations/gateway/` — it has been superseded by the new provider-pattern-based `pkg/gateway/` package (serving
  `/api/v2/gateway/ingestion_keys`)
- Delete dead frontend code: manual IngestionKeys API clients and hooks that targeted the old gateway routes
- Clean up `GatewayApiV1`/`GatewayApiV2` axios instances and route constants from the frontend API layer

## What's retained

- `--gateway-url` flag and `GatewayUrl` field in `APIHandlerOptions` (still used by `cloudIntegrations.go`)
- `pkg/gateway/` package (the new gateway provider)
- `frontend/src/api/generated/services/gateway/` (generated client for the new endpoints)
2026-02-20 16:12:44 +05:30
Yunus M
07afef5c5e fix: use derived values for url and host attributes (#10218)
* fix: use derived values for url and host attributes

* fix: update test cases to use derived attributes

* fix: remove unnecessary handling of http.url and update the test cases

* fix: update SpanDetailsDrawer.test.tsx to use http_url

* fix: repace http_url with constant

* fix: remove redudant url handling in column

* fix: remove endpoint in StatusCodeBarCharts as its fetched from filters

* fix: test title to use correct attribute
2026-02-20 14:20:34 +05:30
SagarRajput-7
dcae722b53 feat: upgraded custom domain and profiles apis and used new open api spec (#10366)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat: upgraded custom domain apis and used new open api spec

* feat: upgraded api in DataSourceInfo

* feat: upgraded the update profile api

* feat: added test cases all the upgraded apis

* feat: updated test cases
2026-02-20 13:02:49 +05:30
105 changed files with 3163 additions and 1762 deletions

View File

@@ -92,6 +92,19 @@ components:
tokenType:
type: string
type: object
AuthtypesGettableTransaction:
properties:
authorized:
type: boolean
object:
$ref: '#/components/schemas/AuthtypesObject'
relation:
type: string
required:
- relation
- object
- authorized
type: object
AuthtypesGoogleConfig:
properties:
allowedGroups:
@@ -117,6 +130,8 @@ components:
serviceAccountJson:
type: string
type: object
AuthtypesName:
type: object
AuthtypesOIDCConfig:
properties:
claimMapping:
@@ -134,6 +149,16 @@ components:
issuerAlias:
type: string
type: object
AuthtypesObject:
properties:
resource:
$ref: '#/components/schemas/AuthtypesResource'
selector:
$ref: '#/components/schemas/AuthtypesSelector'
required:
- resource
- selector
type: object
AuthtypesOrgSessionContext:
properties:
authNSupport:
@@ -171,6 +196,16 @@ components:
refreshToken:
type: string
type: object
AuthtypesResource:
properties:
name:
$ref: '#/components/schemas/AuthtypesName'
type:
type: string
required:
- name
- type
type: object
AuthtypesRoleMapping:
properties:
defaultRole:
@@ -196,6 +231,8 @@ components:
samlIdp:
type: string
type: object
AuthtypesSelector:
type: object
AuthtypesSessionContext:
properties:
exists:
@@ -206,6 +243,18 @@ components:
nullable: true
type: array
type: object
AuthtypesTransaction:
properties:
id:
type: string
object:
$ref: '#/components/schemas/AuthtypesObject'
relation:
type: string
required:
- relation
- object
type: object
AuthtypesUpdateableAuthDomain:
properties:
config:
@@ -277,6 +326,9 @@ components:
type: string
url:
type: string
required:
- code
- message
type: object
ErrorsResponseerroradditional:
properties:
@@ -1612,6 +1664,59 @@ components:
$ref: '#/components/schemas/ErrorsJSON'
status:
type: string
required:
- status
- error
type: object
RoletypesGettableResources:
properties:
relations:
additionalProperties:
items:
type: string
type: array
nullable: true
type: object
resources:
items:
$ref: '#/components/schemas/AuthtypesResource'
nullable: true
type: array
required:
- resources
- relations
type: object
RoletypesPatchableObjects:
properties:
additions:
items:
$ref: '#/components/schemas/AuthtypesObject'
nullable: true
type: array
deletions:
items:
$ref: '#/components/schemas/AuthtypesObject'
nullable: true
type: array
required:
- additions
- deletions
type: object
RoletypesPatchableRole:
properties:
description:
type: string
required:
- description
type: object
RoletypesPostableRole:
properties:
description:
type: string
name:
type: string
required:
- name
type: object
RoletypesRole:
properties:
@@ -1631,6 +1736,11 @@ components:
updatedAt:
format: date-time
type: string
required:
- name
- description
- type
- orgId
type: object
TelemetrytypesFieldContext:
enum:
@@ -2022,6 +2132,44 @@ info:
version: ""
openapi: 3.0.3
paths:
/api/v1/authz/check:
post:
deprecated: false
description: Checks if the authenticated user has permissions for given transactions
operationId: AuthzCheck
requestBody:
content:
application/json:
schema:
items:
$ref: '#/components/schemas/AuthtypesTransaction'
type: array
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/AuthtypesGettableTransaction'
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
summary: Check permissions
tags:
- authz
/api/v1/changePassword/{id}:
post:
deprecated: false
@@ -2094,6 +2242,9 @@ paths:
$ref: '#/components/schemas/AuthtypesGettableToken'
status:
type: string
required:
- status
- data
type: object
description: See Other
"400":
@@ -2132,6 +2283,9 @@ paths:
$ref: '#/components/schemas/AuthtypesGettableToken'
status:
type: string
required:
- status
- data
type: object
description: See Other
"400":
@@ -2195,6 +2349,9 @@ paths:
$ref: '#/components/schemas/AuthtypesGettableToken'
status:
type: string
required:
- status
- data
type: object
description: See Other
"400":
@@ -2289,6 +2446,9 @@ paths:
$ref: '#/components/schemas/DashboardtypesGettablePublicDasbhboard'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -2343,6 +2503,9 @@ paths:
$ref: '#/components/schemas/TypesIdentifiable'
status:
type: string
required:
- status
- data
type: object
description: Created
"401":
@@ -2436,6 +2599,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -2483,6 +2649,9 @@ paths:
$ref: '#/components/schemas/AuthtypesGettableAuthDomain'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -2679,6 +2848,9 @@ paths:
$ref: '#/components/schemas/TelemetrytypesGettableFieldKeys'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -2769,6 +2941,9 @@ paths:
$ref: '#/components/schemas/TelemetrytypesGettableFieldValues'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -2818,6 +2993,9 @@ paths:
$ref: '#/components/schemas/TypesResetPasswordToken'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -2873,6 +3051,9 @@ paths:
$ref: '#/components/schemas/TypesGettableGlobalConfig'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -2918,6 +3099,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -2965,6 +3149,9 @@ paths:
$ref: '#/components/schemas/TypesInvite'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
@@ -3078,6 +3265,9 @@ paths:
$ref: '#/components/schemas/TypesInvite'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -3121,6 +3311,9 @@ paths:
$ref: '#/components/schemas/TypesUser'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
@@ -3215,6 +3408,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -3313,6 +3509,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -3362,6 +3561,9 @@ paths:
$ref: '#/components/schemas/PreferencetypesPreference'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -3475,6 +3677,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -3522,6 +3727,9 @@ paths:
$ref: '#/components/schemas/TypesGettableAPIKey'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
@@ -3689,6 +3897,9 @@ paths:
$ref: '#/components/schemas/DashboardtypesGettablePublicDashboardData'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -3742,6 +3953,9 @@ paths:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -3819,6 +4033,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -3851,6 +4068,11 @@ paths:
deprecated: false
description: This endpoint creates a role
operationId: CreateRole
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/RoletypesPostableRole'
responses:
"201":
content:
@@ -3861,8 +4083,17 @@ paths:
$ref: '#/components/schemas/TypesIdentifiable'
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:
@@ -3875,12 +4106,30 @@ paths:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"451":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unavailable For Legal Reasons
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
"501":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Implemented
security:
- api_key:
- ADMIN
@@ -3919,12 +4168,30 @@ paths:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"451":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unavailable For Legal Reasons
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
"501":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Implemented
security:
- api_key:
- ADMIN
@@ -3953,6 +4220,9 @@ paths:
$ref: '#/components/schemas/RoletypesRole'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -3991,6 +4261,11 @@ paths:
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/RoletypesPatchableRole'
responses:
"204":
content:
@@ -4010,6 +4285,226 @@ paths:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"451":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unavailable For Legal Reasons
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
"501":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Implemented
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Patch role
tags:
- role
/api/v1/roles/{id}/relation/{relation}/objects:
get:
deprecated: false
description: Gets all objects connected to the specified role via a given relation
type
operationId: GetObjects
parameters:
- in: path
name: id
required: true
schema:
type: string
- in: path
name: relation
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/AuthtypesObject'
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
"451":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unavailable For Legal Reasons
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
"501":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Implemented
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Get objects for a role by relation
tags:
- role
patch:
deprecated: false
description: Patches the objects connected to the specified role via a given
relation type
operationId: PatchObjects
parameters:
- in: path
name: id
required: true
schema:
type: string
- in: path
name: relation
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/RoletypesPatchableObjects'
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
"451":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unavailable For Legal Reasons
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
"501":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Implemented
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Patch objects for a role by relation
tags:
- role
/api/v1/roles/resources:
get:
deprecated: false
description: Gets all the available resources for role assignment
operationId: GetResources
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/RoletypesGettableResources'
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:
@@ -4021,7 +4516,7 @@ paths:
- ADMIN
- tokenizer:
- ADMIN
summary: Patch role
summary: Get resources
tags:
- role
/api/v1/user:
@@ -4041,6 +4536,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4135,6 +4633,9 @@ paths:
$ref: '#/components/schemas/TypesUser'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4194,6 +4695,9 @@ paths:
$ref: '#/components/schemas/TypesUser'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -4249,6 +4753,9 @@ paths:
$ref: '#/components/schemas/TypesUser'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4291,6 +4798,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4340,6 +4850,9 @@ paths:
$ref: '#/components/schemas/PreferencetypesPreference'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4482,6 +4995,9 @@ paths:
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4534,6 +5050,9 @@ paths:
$ref: '#/components/schemas/GatewaytypesGettableIngestionKeys'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4581,6 +5100,9 @@ paths:
$ref: '#/components/schemas/GatewaytypesGettableCreatedIngestionKey'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4719,6 +5241,9 @@ paths:
$ref: '#/components/schemas/GatewaytypesGettableCreatedIngestionKeyLimit'
status:
type: string
required:
- status
- data
type: object
description: Created
"401":
@@ -4860,6 +5385,9 @@ paths:
$ref: '#/components/schemas/GatewaytypesGettableIngestionKeys'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -4923,6 +5451,9 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesListMetricsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -4978,6 +5509,9 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesMetricAlertsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5044,6 +5578,9 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesMetricAttributesResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5099,6 +5636,9 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesMetricDashboardsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5155,6 +5695,9 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesMetricHighlightsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5211,6 +5754,9 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesMetricMetadata'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5327,6 +5873,9 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesStatsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5382,6 +5931,9 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesTreemapResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5431,6 +5983,9 @@ paths:
$ref: '#/components/schemas/TypesOrganization'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
@@ -5561,6 +6116,9 @@ paths:
$ref: '#/components/schemas/AuthtypesSessionContext'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5598,6 +6156,9 @@ paths:
$ref: '#/components/schemas/AuthtypesGettableToken'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5641,6 +6202,9 @@ paths:
$ref: '#/components/schemas/AuthtypesGettableToken'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -5673,6 +6237,9 @@ paths:
$ref: '#/components/schemas/ZeustypesGettableHost'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -6187,6 +6754,9 @@ paths:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
@@ -6241,6 +6811,9 @@ paths:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeRequest'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":

View File

@@ -62,10 +62,6 @@ func (provider *provider) Stop(ctx context.Context) error {
return provider.openfgaServer.Stop(ctx)
}
func (provider *provider) Check(ctx context.Context, tuple *openfgav1.TupleKey) error {
return provider.openfgaServer.Check(ctx, tuple)
}
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, roleSelectors []authtypes.Selector) error {
return provider.openfgaServer.CheckWithTupleCreation(ctx, claims, orgID, relation, typeable, selectors, roleSelectors)
}
@@ -74,8 +70,8 @@ func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Contex
return provider.openfgaServer.CheckWithTupleCreationWithoutClaims(ctx, orgID, relation, typeable, selectors, roleSelectors)
}
func (provider *provider) BatchCheck(ctx context.Context, tuples []*openfgav1.TupleKey) error {
return provider.openfgaServer.BatchCheck(ctx, tuples)
func (provider *provider) BatchCheck(ctx context.Context, tupleReq map[string]*openfgav1.TupleKey) (map[string]*authtypes.TupleKeyAuthorization, error) {
return provider.openfgaServer.BatchCheck(ctx, tupleReq)
}
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, typeable authtypes.Typeable) ([]*authtypes.Object, error) {
@@ -187,6 +183,11 @@ func (provider *provider) GetResources(_ context.Context) []*authtypes.Resource
}
func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*authtypes.Object, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
storableRole, err := provider.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
@@ -198,7 +199,7 @@ func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id
resourceObjects, err := provider.
ListObjects(
ctx,
authtypes.MustNewSubject(authtypes.TypeableRole, storableRole.ID.String(), orgID, &authtypes.RelationAssignee),
authtypes.MustNewSubject(authtypes.TypeableRole, storableRole.Name, orgID, &authtypes.RelationAssignee),
relation,
authtypes.MustNewTypeableFromType(resource.Type, resource.Name),
)

View File

@@ -2,8 +2,10 @@ package openfgaserver
import (
"context"
"strconv"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
@@ -28,27 +30,34 @@ func (server *Server) Stop(ctx context.Context) error {
return server.pkgAuthzService.Stop(ctx)
}
func (server *Server) Check(ctx context.Context, tuple *openfgav1.TupleKey) error {
return server.pkgAuthzService.Check(ctx, tuple)
}
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, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
if err != nil {
return err
}
tuples, err := typeable.Tuples(subject, relation, selectors, orgID)
tupleSlice, err := typeable.Tuples(subject, relation, selectors, orgID)
if err != nil {
return err
}
err = server.BatchCheck(ctx, tuples)
tuples := make(map[string]*openfgav1.TupleKey, len(tupleSlice))
for idx, tuple := range tupleSlice {
tuples[strconv.Itoa(idx)] = tuple
}
response, err := server.BatchCheck(ctx, tuples)
if err != nil {
return err
}
return nil
for _, resp := range response {
if resp.Authorized {
return nil
}
}
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "subjects are not authorized for requested access")
}
func (server *Server) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, _ []authtypes.Selector) error {
@@ -57,21 +66,32 @@ func (server *Server) CheckWithTupleCreationWithoutClaims(ctx context.Context, o
return err
}
tuples, err := typeable.Tuples(subject, relation, selectors, orgID)
tupleSlice, err := typeable.Tuples(subject, relation, selectors, orgID)
if err != nil {
return err
}
err = server.BatchCheck(ctx, tuples)
tuples := make(map[string]*openfgav1.TupleKey, len(tupleSlice))
for idx, tuple := range tupleSlice {
tuples[strconv.Itoa(idx)] = tuple
}
response, err := server.BatchCheck(ctx, tuples)
if err != nil {
return err
}
return nil
for _, resp := range response {
if resp.Authorized {
return nil
}
}
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "subjects are not authorized for requested access")
}
func (server *Server) BatchCheck(ctx context.Context, tuples []*openfgav1.TupleKey) error {
return server.pkgAuthzService.BatchCheck(ctx, tuples)
func (server *Server) BatchCheck(ctx context.Context, tupleReq map[string]*openfgav1.TupleKey) (map[string]*authtypes.TupleKeyAuthorization, error) {
return server.pkgAuthzService.BatchCheck(ctx, tupleReq)
}
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, typeable authtypes.Typeable) ([]*authtypes.Object, error) {

View File

@@ -220,6 +220,7 @@ func (module *module) MustGetManagedRoleTransactions() map[string][]*authtypes.T
return map[string][]*authtypes.Transaction{
roletypes.SigNozAnonymousRoleName: {
{
ID: valuer.GenerateUUID(),
Relation: authtypes.RelationRead,
Object: *authtypes.MustNewObject(
authtypes.Resource{

View File

@@ -2,11 +2,9 @@ package api
import (
"net/http"
"net/http/httputil"
"time"
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/global"
@@ -31,7 +29,6 @@ type APIHandlerOptions struct {
IntegrationsController *integrations.Controller
CloudIntegrationsController *cloudintegrations.Controller
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
Gateway *httputil.ReverseProxy
GatewayUrl string
// Querier Influx Interval
FluxInterval time.Duration
@@ -77,10 +74,6 @@ func (ah *APIHandler) UM() *usage.Manager {
return ah.opts.UsageManager
}
func (ah *APIHandler) Gateway() *httputil.ReverseProxy {
return ah.opts.Gateway
}
// RegisterRoutes registers routes for this handler on the given router
func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
// note: add ee override methods first
@@ -103,9 +96,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
// v4
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
// Gateway
router.PathPrefix(gateway.RoutePrefix).HandlerFunc(am.EditAccess(ah.ServeGatewayHTTP))
ah.APIHandler.RegisterRoutes(router, am)
}

View File

@@ -1,58 +0,0 @@
package api
import (
"net/http"
"strings"
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
validPath := false
for _, allowedPrefix := range gateway.AllowedPrefix {
if strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+allowedPrefix) {
validPath = true
break
}
}
if !validPath {
rw.WriteHeader(http.StatusNotFound)
return
}
license, err := ah.Signoz.Licensing.GetActive(ctx, orgID)
if err != nil {
render.Error(rw, err)
return
}
//Create headers
var licenseKey string
if license != nil {
licenseKey = license.Key
}
req.Header.Set("X-Signoz-Cloud-Api-Key", licenseKey)
req.Header.Set("X-Consumer-Username", "lid:00000000-0000-0000-0000-000000000000")
req.Header.Set("X-Consumer-Groups", "ns:default")
ah.Gateway().ServeHTTP(rw, req)
}

View File

@@ -19,7 +19,6 @@ import (
"github.com/gorilla/handlers"
"github.com/SigNoz/signoz/ee/query-service/app/api"
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
"github.com/SigNoz/signoz/ee/query-service/rules"
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager"
@@ -72,11 +71,6 @@ type Server struct {
// NewServer creates and initializes Server
func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
gatewayProxy, err := gateway.NewProxy(config.Gateway.URL.String(), gateway.RoutePrefix)
if err != nil {
return nil, err
}
cacheForTraceDetail, err := memorycache.New(context.TODO(), signoz.Instrumentation.ToProviderSettings(), cache.Config{
Provider: "memory",
Memory: cache.Memory{
@@ -170,7 +164,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
CloudIntegrationsController: cloudIntegrationsController,
LogsParsingPipelineController: logParsingPipelineController,
FluxInterval: config.Querier.FluxInterval,
Gateway: gatewayProxy,
GatewayUrl: config.Gateway.URL.String(),
GlobalConfig: config.Global,
}

View File

@@ -1,9 +0,0 @@
package gateway
import (
"net/http/httputil"
)
func NewNoopProxy() (*httputil.ReverseProxy, error) {
return &httputil.ReverseProxy{}, nil
}

View File

@@ -1,66 +0,0 @@
package gateway
import (
"net/http"
"net/http/httputil"
"net/url"
"path"
"strings"
)
var (
RoutePrefix string = "/api/gateway"
AllowedPrefix []string = []string{"/v1/workspaces/me", "/v2/profiles/me", "/v2/deployments/me"}
)
type proxy struct {
url *url.URL
stripPath string
}
func NewProxy(u string, stripPath string) (*httputil.ReverseProxy, error) {
url, err := url.Parse(u)
if err != nil {
return nil, err
}
proxy := &proxy{url: url, stripPath: stripPath}
return &httputil.ReverseProxy{
Rewrite: proxy.rewrite,
ModifyResponse: proxy.modifyResponse,
ErrorHandler: proxy.errorHandler,
}, nil
}
func (p *proxy) rewrite(pr *httputil.ProxyRequest) {
pr.SetURL(p.url)
pr.SetXForwarded()
pr.Out.URL.Path = cleanPath(strings.ReplaceAll(pr.Out.URL.Path, p.stripPath, ""))
}
func (p *proxy) modifyResponse(res *http.Response) error {
return nil
}
func (p *proxy) errorHandler(rw http.ResponseWriter, req *http.Request, err error) {
rw.WriteHeader(http.StatusBadGateway)
}
func cleanPath(p string) string {
if p == "" {
return "/"
}
if p[0] != '/' {
p = "/" + p
}
np := path.Clean(p)
if p[len(p)-1] == '/' && np != "/" {
if len(p) == len(np)+1 && strings.HasPrefix(p, np) {
np = p
} else {
np += "/"
}
}
return np
}

View File

@@ -1,61 +0,0 @@
package gateway
import (
"context"
"net/http"
"net/http/httputil"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestProxyRewrite(t *testing.T) {
testCases := []struct {
name string
url *url.URL
stripPath string
in *url.URL
expected *url.URL
}{
{
name: "SamePathAdded",
url: &url.URL{Scheme: "http", Host: "backend", Path: "/path1"},
stripPath: "/strip",
in: &url.URL{Scheme: "http", Host: "localhost", Path: "/strip/path1"},
expected: &url.URL{Scheme: "http", Host: "backend", Path: "/path1/path1"},
},
{
name: "NoStripPathInput",
url: &url.URL{Scheme: "http", Host: "backend"},
stripPath: "",
in: &url.URL{Scheme: "http", Host: "localhost", Path: "/strip/path1"},
expected: &url.URL{Scheme: "http", Host: "backend", Path: "/strip/path1"},
},
{
name: "NoStripPathPresentInReq",
url: &url.URL{Scheme: "http", Host: "backend"},
stripPath: "/not-found",
in: &url.URL{Scheme: "http", Host: "localhost", Path: "/strip/path1"},
expected: &url.URL{Scheme: "http", Host: "backend", Path: "/strip/path1"},
},
}
for _, tc := range testCases {
proxy, err := NewProxy(tc.url.String(), tc.stripPath)
require.NoError(t, err)
inReq, err := http.NewRequest(http.MethodGet, tc.in.String(), nil)
require.NoError(t, err)
proxyReq := &httputil.ProxyRequest{
In: inReq,
Out: inReq.Clone(context.Background()),
}
proxy.Rewrite(proxyReq)
assert.Equal(t, tc.expected.Host, proxyReq.Out.URL.Host)
assert.Equal(t, tc.expected.Scheme, proxyReq.Out.URL.Scheme)
assert.Equal(t, tc.expected.Path, proxyReq.Out.URL.Path)
assert.Equal(t, tc.expected.Query(), proxyReq.Out.URL.Query())
}
}

View File

@@ -1,29 +0,0 @@
import { GatewayApiV1Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
CreateIngestionKeyProps,
IngestionKeyProps,
} from 'types/api/ingestionKeys/types';
const createIngestionKey = async (
props: CreateIngestionKeyProps,
): Promise<SuccessResponse<IngestionKeyProps> | ErrorResponse> => {
try {
const response = await GatewayApiV1Instance.post('/workspaces/me/keys', {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default createIngestionKey;

View File

@@ -1,26 +0,0 @@
import { GatewayApiV1Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AllIngestionKeyProps } from 'types/api/ingestionKeys/types';
const deleteIngestionKey = async (
id: string,
): Promise<SuccessResponse<AllIngestionKeyProps> | ErrorResponse> => {
try {
const response = await GatewayApiV1Instance.delete(
`/workspaces/me/keys/${id}`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default deleteIngestionKey;

View File

@@ -1,21 +0,0 @@
import { GatewayApiV1Instance } from 'api';
import { AxiosResponse } from 'axios';
import {
AllIngestionKeyProps,
GetIngestionKeyProps,
} from 'types/api/ingestionKeys/types';
export const getAllIngestionKeys = (
props: GetIngestionKeyProps,
): Promise<AxiosResponse<AllIngestionKeyProps>> => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { search, per_page, page } = props;
const BASE_URL = '/workspaces/me/keys';
const URL_QUERY_PARAMS =
search && search.length > 0
? `/search?name=${search}&page=1&per_page=100`
: `?page=${page}&per_page=${per_page}`;
return GatewayApiV1Instance.get(`${BASE_URL}${URL_QUERY_PARAMS}`);
};

View File

@@ -1,65 +0,0 @@
/* eslint-disable @typescript-eslint/no-throw-literal */
import { GatewayApiV1Instance } from 'api';
import axios from 'axios';
import {
AddLimitProps,
LimitSuccessProps,
} from 'types/api/ingestionKeys/limits/types';
interface SuccessResponse<T> {
statusCode: number;
error: null;
message: string;
payload: T;
}
interface ErrorResponse {
statusCode: number;
error: string;
message: string;
payload: null;
}
const createLimitForIngestionKey = async (
props: AddLimitProps,
): Promise<SuccessResponse<LimitSuccessProps> | ErrorResponse> => {
try {
const response = await GatewayApiV1Instance.post(
`/workspaces/me/keys/${props.keyID}/limits`,
{
...props,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
if (axios.isAxiosError(error)) {
// Axios error
const errResponse: ErrorResponse = {
statusCode: error.response?.status || 500,
error: error.response?.data?.error,
message: error.response?.data?.status || 'An error occurred',
payload: null,
};
throw errResponse;
} else {
// Non-Axios error
const errResponse: ErrorResponse = {
statusCode: 500,
error: 'Unknown error',
message: 'An unknown error occurred',
payload: null,
};
throw errResponse;
}
}
};
export default createLimitForIngestionKey;

View File

@@ -1,26 +0,0 @@
import { GatewayApiV1Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AllIngestionKeyProps } from 'types/api/ingestionKeys/types';
const deleteLimitsForIngestionKey = async (
id: string,
): Promise<SuccessResponse<AllIngestionKeyProps> | ErrorResponse> => {
try {
const response = await GatewayApiV1Instance.delete(
`/workspaces/me/limits/${id}`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default deleteLimitsForIngestionKey;

View File

@@ -1,65 +0,0 @@
/* eslint-disable @typescript-eslint/no-throw-literal */
import { GatewayApiV1Instance } from 'api';
import axios from 'axios';
import {
LimitSuccessProps,
UpdateLimitProps,
} from 'types/api/ingestionKeys/limits/types';
interface SuccessResponse<T> {
statusCode: number;
error: null;
message: string;
payload: T;
}
interface ErrorResponse {
statusCode: number;
error: string;
message: string;
payload: null;
}
const updateLimitForIngestionKey = async (
props: UpdateLimitProps,
): Promise<SuccessResponse<LimitSuccessProps> | ErrorResponse> => {
try {
const response = await GatewayApiV1Instance.patch(
`/workspaces/me/limits/${props.limitID}`,
{
config: props.config,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
if (axios.isAxiosError(error)) {
// Axios error
const errResponse: ErrorResponse = {
statusCode: error.response?.status || 500,
error: error.response?.data?.error,
message: error.response?.data?.status || 'An error occurred',
payload: null,
};
throw errResponse;
} else {
// Non-Axios error
const errResponse: ErrorResponse = {
statusCode: 500,
error: 'Unknown error',
message: 'An unknown error occurred',
payload: null,
};
throw errResponse;
}
}
};
export default updateLimitForIngestionKey;

View File

@@ -1,32 +0,0 @@
import { GatewayApiV1Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
IngestionKeysPayloadProps,
UpdateIngestionKeyProps,
} from 'types/api/ingestionKeys/types';
const updateIngestionKey = async (
props: UpdateIngestionKeyProps,
): Promise<SuccessResponse<IngestionKeysPayloadProps> | ErrorResponse> => {
try {
const response = await GatewayApiV1Instance.patch(
`/workspaces/me/keys/${props.id}`,
{
...props.data,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default updateIngestionKey;

View File

@@ -4,8 +4,6 @@ export const apiV2 = '/api/v2/';
export const apiV3 = '/api/v3/';
export const apiV4 = '/api/v4/';
export const apiV5 = '/api/v5/';
export const gatewayApiV1 = '/api/gateway/v1/';
export const gatewayApiV2 = '/api/gateway/v2/';
export const apiAlertManager = '/api/alertmanager/';
export default apiV1;

View File

@@ -1,7 +0,0 @@
import { GatewayApiV2Instance as axios } from 'api';
import { AxiosResponse } from 'axios';
import { DeploymentsDataProps } from 'types/api/customDomain/types';
export const getDeploymentsData = (): Promise<
AxiosResponse<DeploymentsDataProps>
> => axios.get(`/deployments/me`);

View File

@@ -1,16 +0,0 @@
import { GatewayApiV2Instance as axios } from 'api';
import { AxiosError } from 'axios';
import { SuccessResponse } from 'types/api';
import {
PayloadProps,
UpdateCustomDomainProps,
} from 'types/api/customDomain/types';
const updateSubDomainAPI = async (
props: UpdateCustomDomainProps,
): Promise<SuccessResponse<PayloadProps> | AxiosError> =>
axios.put(`/deployments/me/host`, {
...props.data,
});
export default updateSubDomainAPI;

View File

@@ -0,0 +1,107 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
MutationFunction,
UseMutationOptions,
UseMutationResult,
} from 'react-query';
import { useMutation } from 'react-query';
import { GeneratedAPIInstance } from '../../../index';
import type {
AuthtypesTransactionDTO,
AuthzCheck200,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* Checks if the authenticated user has permissions for given transactions
* @summary Check permissions
*/
export const authzCheck = (
authtypesTransactionDTO: AuthtypesTransactionDTO[],
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<AuthzCheck200>({
url: `/api/v1/authz/check`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: authtypesTransactionDTO,
signal,
});
};
export const getAuthzCheckMutationOptions = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof authzCheck>>,
TError,
{ data: AuthtypesTransactionDTO[] },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof authzCheck>>,
TError,
{ data: AuthtypesTransactionDTO[] },
TContext
> => {
const mutationKey = ['authzCheck'];
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 authzCheck>>,
{ data: AuthtypesTransactionDTO[] }
> = (props) => {
const { data } = props ?? {};
return authzCheck(data);
};
return { mutationFn, ...mutationOptions };
};
export type AuthzCheckMutationResult = NonNullable<
Awaited<ReturnType<typeof authzCheck>>
>;
export type AuthzCheckMutationBody = AuthtypesTransactionDTO[];
export type AuthzCheckMutationError = RenderErrorResponseDTO;
/**
* @summary Check permissions
*/
export const useAuthzCheck = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof authzCheck>>,
TError,
{ data: AuthtypesTransactionDTO[] },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof authzCheck>>,
TError,
{ data: AuthtypesTransactionDTO[] },
TContext
> => {
const mutationOptions = getAuthzCheckMutationOptions(options);
return useMutation(mutationOptions);
};

View File

@@ -21,11 +21,18 @@ import { GeneratedAPIInstance } from '../../../index';
import type {
CreateRole201,
DeleteRolePathParameters,
GetObjects200,
GetObjectsPathParameters,
GetResources200,
GetRole200,
GetRolePathParameters,
ListRoles200,
PatchObjectsPathParameters,
PatchRolePathParameters,
RenderErrorResponseDTO,
RoletypesPatchableObjectsDTO,
RoletypesPatchableRoleDTO,
RoletypesPostableRoleDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
@@ -114,10 +121,15 @@ export const invalidateListRoles = async (
* This endpoint creates a role
* @summary Create role
*/
export const createRole = (signal?: AbortSignal) => {
export const createRole = (
roletypesPostableRoleDTO: RoletypesPostableRoleDTO,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateRole201>({
url: `/api/v1/roles`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: roletypesPostableRoleDTO,
signal,
});
};
@@ -129,13 +141,13 @@ export const getCreateRoleMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
void,
{ data: RoletypesPostableRoleDTO },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
void,
{ data: RoletypesPostableRoleDTO },
TContext
> => {
const mutationKey = ['createRole'];
@@ -149,9 +161,11 @@ export const getCreateRoleMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createRole>>,
void
> = () => {
return createRole();
{ data: RoletypesPostableRoleDTO }
> = (props) => {
const { data } = props ?? {};
return createRole(data);
};
return { mutationFn, ...mutationOptions };
@@ -160,7 +174,7 @@ export const getCreateRoleMutationOptions = <
export type CreateRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof createRole>>
>;
export type CreateRoleMutationBody = RoletypesPostableRoleDTO;
export type CreateRoleMutationError = RenderErrorResponseDTO;
/**
@@ -173,13 +187,13 @@ export const useCreateRole = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
void,
{ data: RoletypesPostableRoleDTO },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createRole>>,
TError,
void,
{ data: RoletypesPostableRoleDTO },
TContext
> => {
const mutationOptions = getCreateRoleMutationOptions(options);
@@ -358,10 +372,15 @@ export const invalidateGetRole = async (
* This endpoint patches a role
* @summary Patch role
*/
export const patchRole = ({ id }: PatchRolePathParameters) => {
export const patchRole = (
{ id }: PatchRolePathParameters,
roletypesPatchableRoleDTO: RoletypesPatchableRoleDTO,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/roles/${id}`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
data: roletypesPatchableRoleDTO,
});
};
@@ -372,13 +391,13 @@ export const getPatchRoleMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof patchRole>>,
TError,
{ pathParams: PatchRolePathParameters },
{ pathParams: PatchRolePathParameters; data: RoletypesPatchableRoleDTO },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof patchRole>>,
TError,
{ pathParams: PatchRolePathParameters },
{ pathParams: PatchRolePathParameters; data: RoletypesPatchableRoleDTO },
TContext
> => {
const mutationKey = ['patchRole'];
@@ -392,11 +411,11 @@ export const getPatchRoleMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof patchRole>>,
{ pathParams: PatchRolePathParameters }
{ pathParams: PatchRolePathParameters; data: RoletypesPatchableRoleDTO }
> = (props) => {
const { pathParams } = props ?? {};
const { pathParams, data } = props ?? {};
return patchRole(pathParams);
return patchRole(pathParams, data);
};
return { mutationFn, ...mutationOptions };
@@ -405,7 +424,7 @@ export const getPatchRoleMutationOptions = <
export type PatchRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof patchRole>>
>;
export type PatchRoleMutationBody = RoletypesPatchableRoleDTO;
export type PatchRoleMutationError = RenderErrorResponseDTO;
/**
@@ -418,16 +437,292 @@ export const usePatchRole = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof patchRole>>,
TError,
{ pathParams: PatchRolePathParameters },
{ pathParams: PatchRolePathParameters; data: RoletypesPatchableRoleDTO },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof patchRole>>,
TError,
{ pathParams: PatchRolePathParameters },
{ pathParams: PatchRolePathParameters; data: RoletypesPatchableRoleDTO },
TContext
> => {
const mutationOptions = getPatchRoleMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Gets all objects connected to the specified role via a given relation type
* @summary Get objects for a role by relation
*/
export const getObjects = (
{ id, relation }: GetObjectsPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetObjects200>({
url: `/api/v1/roles/${id}/relation/${relation}/objects`,
method: 'GET',
signal,
});
};
export const getGetObjectsQueryKey = ({
id,
relation,
}: GetObjectsPathParameters) => {
return ['getObjects'] as const;
};
export const getGetObjectsQueryOptions = <
TData = Awaited<ReturnType<typeof getObjects>>,
TError = RenderErrorResponseDTO
>(
{ id, relation }: GetObjectsPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getObjects>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetObjectsQueryKey({ id, relation });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getObjects>>> = ({
signal,
}) => getObjects({ id, relation }, signal);
return {
queryKey,
queryFn,
enabled: !!(id && relation),
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof getObjects>>, TError, TData> & {
queryKey: QueryKey;
};
};
export type GetObjectsQueryResult = NonNullable<
Awaited<ReturnType<typeof getObjects>>
>;
export type GetObjectsQueryError = RenderErrorResponseDTO;
/**
* @summary Get objects for a role by relation
*/
export function useGetObjects<
TData = Awaited<ReturnType<typeof getObjects>>,
TError = RenderErrorResponseDTO
>(
{ id, relation }: GetObjectsPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getObjects>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetObjectsQueryOptions({ id, relation }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get objects for a role by relation
*/
export const invalidateGetObjects = async (
queryClient: QueryClient,
{ id, relation }: GetObjectsPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetObjectsQueryKey({ id, relation }) },
options,
);
return queryClient;
};
/**
* Patches the objects connected to the specified role via a given relation type
* @summary Patch objects for a role by relation
*/
export const patchObjects = (
{ id, relation }: PatchObjectsPathParameters,
roletypesPatchableObjectsDTO: RoletypesPatchableObjectsDTO,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/roles/${id}/relation/${relation}/objects`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
data: roletypesPatchableObjectsDTO,
});
};
export const getPatchObjectsMutationOptions = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof patchObjects>>,
TError,
{
pathParams: PatchObjectsPathParameters;
data: RoletypesPatchableObjectsDTO;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof patchObjects>>,
TError,
{ pathParams: PatchObjectsPathParameters; data: RoletypesPatchableObjectsDTO },
TContext
> => {
const mutationKey = ['patchObjects'];
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 patchObjects>>,
{ pathParams: PatchObjectsPathParameters; data: RoletypesPatchableObjectsDTO }
> = (props) => {
const { pathParams, data } = props ?? {};
return patchObjects(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type PatchObjectsMutationResult = NonNullable<
Awaited<ReturnType<typeof patchObjects>>
>;
export type PatchObjectsMutationBody = RoletypesPatchableObjectsDTO;
export type PatchObjectsMutationError = RenderErrorResponseDTO;
/**
* @summary Patch objects for a role by relation
*/
export const usePatchObjects = <
TError = RenderErrorResponseDTO,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof patchObjects>>,
TError,
{
pathParams: PatchObjectsPathParameters;
data: RoletypesPatchableObjectsDTO;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof patchObjects>>,
TError,
{ pathParams: PatchObjectsPathParameters; data: RoletypesPatchableObjectsDTO },
TContext
> => {
const mutationOptions = getPatchObjectsMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Gets all the available resources for role assignment
* @summary Get resources
*/
export const getResources = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetResources200>({
url: `/api/v1/roles/resources`,
method: 'GET',
signal,
});
};
export const getGetResourcesQueryKey = () => {
return ['getResources'] as const;
};
export const getGetResourcesQueryOptions = <
TData = Awaited<ReturnType<typeof getResources>>,
TError = RenderErrorResponseDTO
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getResources>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetResourcesQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getResources>>> = ({
signal,
}) => getResources(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getResources>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetResourcesQueryResult = NonNullable<
Awaited<ReturnType<typeof getResources>>
>;
export type GetResourcesQueryError = RenderErrorResponseDTO;
/**
* @summary Get resources
*/
export function useGetResources<
TData = Awaited<ReturnType<typeof getResources>>,
TError = RenderErrorResponseDTO
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getResources>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetResourcesQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get resources
*/
export const invalidateGetResources = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetResourcesQueryKey() },
options,
);
return queryClient;
};

View File

@@ -127,6 +127,18 @@ export interface AuthtypesGettableTokenDTO {
tokenType?: string;
}
export interface AuthtypesGettableTransactionDTO {
/**
* @type boolean
*/
authorized: boolean;
object: AuthtypesObjectDTO;
/**
* @type string
*/
relation: string;
}
export type AuthtypesGoogleConfigDTODomainToAdminEmail = {
[key: string]: string;
};
@@ -170,6 +182,10 @@ export interface AuthtypesGoogleConfigDTO {
serviceAccountJson?: string;
}
export interface AuthtypesNameDTO {
[key: string]: unknown;
}
export interface AuthtypesOIDCConfigDTO {
claimMapping?: AuthtypesAttributeMappingDTO;
/**
@@ -198,6 +214,11 @@ export interface AuthtypesOIDCConfigDTO {
issuerAlias?: string;
}
export interface AuthtypesObjectDTO {
resource: AuthtypesResourceDTO;
selector: AuthtypesSelectorDTO;
}
export interface AuthtypesOrgSessionContextDTO {
authNSupport?: AuthtypesAuthNSupportDTO;
/**
@@ -248,6 +269,14 @@ export interface AuthtypesPostableRotateTokenDTO {
refreshToken?: string;
}
export interface AuthtypesResourceDTO {
name: AuthtypesNameDTO;
/**
* @type string
*/
type: string;
}
/**
* @nullable
*/
@@ -291,6 +320,10 @@ export interface AuthtypesSamlConfigDTO {
samlIdp?: string;
}
export interface AuthtypesSelectorDTO {
[key: string]: unknown;
}
export interface AuthtypesSessionContextDTO {
/**
* @type boolean
@@ -303,6 +336,18 @@ export interface AuthtypesSessionContextDTO {
orgs?: AuthtypesOrgSessionContextDTO[] | null;
}
export interface AuthtypesTransactionDTO {
/**
* @type string
*/
id?: string;
object: AuthtypesObjectDTO;
/**
* @type string
*/
relation: string;
}
export interface AuthtypesUpdateableAuthDomainDTO {
config?: AuthtypesAuthDomainConfigDTO;
}
@@ -391,7 +436,7 @@ export interface ErrorsJSONDTO {
/**
* @type string
*/
code?: string;
code: string;
/**
* @type array
*/
@@ -399,7 +444,7 @@ export interface ErrorsJSONDTO {
/**
* @type string
*/
message?: string;
message: string;
/**
* @type string
*/
@@ -1940,11 +1985,62 @@ export enum Querybuildertypesv5VariableTypeDTO {
text = 'text',
}
export interface RenderErrorResponseDTO {
error?: ErrorsJSONDTO;
error: ErrorsJSONDTO;
/**
* @type string
*/
status?: string;
status: string;
}
/**
* @nullable
*/
export type RoletypesGettableResourcesDTORelations = {
[key: string]: string[];
} | null;
export interface RoletypesGettableResourcesDTO {
/**
* @type object
* @nullable true
*/
relations: RoletypesGettableResourcesDTORelations;
/**
* @type array
* @nullable true
*/
resources: AuthtypesResourceDTO[] | null;
}
export interface RoletypesPatchableObjectsDTO {
/**
* @type array
* @nullable true
*/
additions: AuthtypesObjectDTO[] | null;
/**
* @type array
* @nullable true
*/
deletions: AuthtypesObjectDTO[] | null;
}
export interface RoletypesPatchableRoleDTO {
/**
* @type string
*/
description: string;
}
export interface RoletypesPostableRoleDTO {
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name: string;
}
export interface RoletypesRoleDTO {
@@ -1956,7 +2052,7 @@ export interface RoletypesRoleDTO {
/**
* @type string
*/
description?: string;
description: string;
/**
* @type string
*/
@@ -1964,15 +2060,15 @@ export interface RoletypesRoleDTO {
/**
* @type string
*/
name?: string;
name: string;
/**
* @type string
*/
orgId?: string;
orgId: string;
/**
* @type string
*/
type?: string;
type: string;
/**
* @type string
* @format date-time
@@ -2499,23 +2595,34 @@ export interface ZeustypesPostableProfileDTO {
where_did_you_discover_signoz: string;
}
export type AuthzCheck200 = {
/**
* @type array
*/
data: AuthtypesGettableTransactionDTO[];
/**
* @type string
*/
status: string;
};
export type ChangePasswordPathParameters = {
id: string;
};
export type CreateSessionByGoogleCallback303 = {
data?: AuthtypesGettableTokenDTO;
data: AuthtypesGettableTokenDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type CreateSessionByOIDCCallback303 = {
data?: AuthtypesGettableTokenDTO;
data: AuthtypesGettableTokenDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type CreateSessionBySAMLCallbackParams = {
@@ -2543,11 +2650,11 @@ export type CreateSessionBySAMLCallbackBody = {
};
export type CreateSessionBySAMLCallback303 = {
data?: AuthtypesGettableTokenDTO;
data: AuthtypesGettableTokenDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type DeletePublicDashboardPathParameters = {
@@ -2557,22 +2664,22 @@ export type GetPublicDashboardPathParameters = {
id: string;
};
export type GetPublicDashboard200 = {
data?: DashboardtypesGettablePublicDasbhboardDTO;
data: DashboardtypesGettablePublicDasbhboardDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type CreatePublicDashboardPathParameters = {
id: string;
};
export type CreatePublicDashboard201 = {
data?: TypesIdentifiableDTO;
data: TypesIdentifiableDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type UpdatePublicDashboardPathParameters = {
@@ -2582,19 +2689,19 @@ export type ListAuthDomains200 = {
/**
* @type array
*/
data?: AuthtypesGettableAuthDomainDTO[];
data: AuthtypesGettableAuthDomainDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type CreateAuthDomain200 = {
data?: AuthtypesGettableAuthDomainDTO;
data: AuthtypesGettableAuthDomainDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type DeleteAuthDomainPathParameters = {
@@ -2650,11 +2757,11 @@ export type GetFieldsKeysParams = {
};
export type GetFieldsKeys200 = {
data?: TelemetrytypesGettableFieldKeysDTO;
data: TelemetrytypesGettableFieldKeysDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetFieldsValuesParams = {
@@ -2714,49 +2821,49 @@ export type GetFieldsValuesParams = {
};
export type GetFieldsValues200 = {
data?: TelemetrytypesGettableFieldValuesDTO;
data: TelemetrytypesGettableFieldValuesDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetResetPasswordTokenPathParameters = {
id: string;
};
export type GetResetPasswordToken200 = {
data?: TypesResetPasswordTokenDTO;
data: TypesResetPasswordTokenDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetGlobalConfig200 = {
data?: TypesGettableGlobalConfigDTO;
data: TypesGettableGlobalConfigDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type ListInvite200 = {
/**
* @type array
*/
data?: TypesInviteDTO[];
data: TypesInviteDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type CreateInvite201 = {
data?: TypesInviteDTO;
data: TypesInviteDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type DeleteInvitePathParameters = {
@@ -2766,19 +2873,19 @@ export type GetInvitePathParameters = {
token: string;
};
export type GetInvite200 = {
data?: TypesInviteDTO;
data: TypesInviteDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type AcceptInvite201 = {
data?: TypesUserDTO;
data: TypesUserDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type ListPromotedAndIndexedPaths200 = {
@@ -2786,33 +2893,33 @@ export type ListPromotedAndIndexedPaths200 = {
* @type array
* @nullable true
*/
data?: PromotetypesPromotePathDTO[] | null;
data: PromotetypesPromotePathDTO[] | null;
/**
* @type string
*/
status?: string;
status: string;
};
export type ListOrgPreferences200 = {
/**
* @type array
*/
data?: PreferencetypesPreferenceDTO[];
data: PreferencetypesPreferenceDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type GetOrgPreferencePathParameters = {
name: string;
};
export type GetOrgPreference200 = {
data?: PreferencetypesPreferenceDTO;
data: PreferencetypesPreferenceDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type UpdateOrgPreferencePathParameters = {
@@ -2822,19 +2929,19 @@ export type ListAPIKeys200 = {
/**
* @type array
*/
data?: TypesGettableAPIKeyDTO[];
data: TypesGettableAPIKeyDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type CreateAPIKey201 = {
data?: TypesGettableAPIKeyDTO;
data: TypesGettableAPIKeyDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type RevokeAPIKeyPathParameters = {
@@ -2847,11 +2954,11 @@ export type GetPublicDashboardDataPathParameters = {
id: string;
};
export type GetPublicDashboardData200 = {
data?: DashboardtypesGettablePublicDashboardDataDTO;
data: DashboardtypesGettablePublicDashboardDataDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetPublicDashboardWidgetQueryRangePathParameters = {
@@ -2859,30 +2966,30 @@ export type GetPublicDashboardWidgetQueryRangePathParameters = {
idx: string;
};
export type GetPublicDashboardWidgetQueryRange200 = {
data?: Querybuildertypesv5QueryRangeResponseDTO;
data: Querybuildertypesv5QueryRangeResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type ListRoles200 = {
/**
* @type array
*/
data?: RoletypesRoleDTO[];
data: RoletypesRoleDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type CreateRole201 = {
data?: TypesIdentifiableDTO;
data: TypesIdentifiableDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type DeleteRolePathParameters = {
@@ -2892,25 +2999,52 @@ export type GetRolePathParameters = {
id: string;
};
export type GetRole200 = {
data?: RoletypesRoleDTO;
data: RoletypesRoleDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type PatchRolePathParameters = {
id: string;
};
export type GetObjectsPathParameters = {
id: string;
relation: string;
};
export type GetObjects200 = {
/**
* @type array
*/
data: AuthtypesObjectDTO[];
/**
* @type string
*/
status: string;
};
export type PatchObjectsPathParameters = {
id: string;
relation: string;
};
export type GetResources200 = {
data: RoletypesGettableResourcesDTO;
/**
* @type string
*/
status: string;
};
export type ListUsers200 = {
/**
* @type array
*/
data?: TypesUserDTO[];
data: TypesUserDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type DeleteUserPathParameters = {
@@ -2920,52 +3054,52 @@ export type GetUserPathParameters = {
id: string;
};
export type GetUser200 = {
data?: TypesUserDTO;
data: TypesUserDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type UpdateUserPathParameters = {
id: string;
};
export type UpdateUser200 = {
data?: TypesUserDTO;
data: TypesUserDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetMyUser200 = {
data?: TypesUserDTO;
data: TypesUserDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type ListUserPreferences200 = {
/**
* @type array
*/
data?: PreferencetypesPreferenceDTO[];
data: PreferencetypesPreferenceDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type GetUserPreferencePathParameters = {
name: string;
};
export type GetUserPreference200 = {
data?: PreferencetypesPreferenceDTO;
data: PreferencetypesPreferenceDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type UpdateUserPreferencePathParameters = {
@@ -2975,11 +3109,11 @@ export type GetFeatures200 = {
/**
* @type array
*/
data?: FeaturetypesGettableFeatureDTO[];
data: FeaturetypesGettableFeatureDTO[];
/**
* @type string
*/
status?: string;
status: string;
};
export type GetIngestionKeysParams = {
@@ -2996,19 +3130,19 @@ export type GetIngestionKeysParams = {
};
export type GetIngestionKeys200 = {
data?: GatewaytypesGettableIngestionKeysDTO;
data: GatewaytypesGettableIngestionKeysDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type CreateIngestionKey200 = {
data?: GatewaytypesGettableCreatedIngestionKeyDTO;
data: GatewaytypesGettableCreatedIngestionKeyDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type DeleteIngestionKeyPathParameters = {
@@ -3021,11 +3155,11 @@ export type CreateIngestionKeyLimitPathParameters = {
keyId: string;
};
export type CreateIngestionKeyLimit201 = {
data?: GatewaytypesGettableCreatedIngestionKeyLimitDTO;
data: GatewaytypesGettableCreatedIngestionKeyLimitDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type DeleteIngestionKeyLimitPathParameters = {
@@ -3053,11 +3187,11 @@ export type SearchIngestionKeysParams = {
};
export type SearchIngestionKeys200 = {
data?: GatewaytypesGettableIngestionKeysDTO;
data: GatewaytypesGettableIngestionKeysDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type ListMetricsParams = {
@@ -3086,22 +3220,22 @@ export type ListMetricsParams = {
};
export type ListMetrics200 = {
data?: MetricsexplorertypesListMetricsResponseDTO;
data: MetricsexplorertypesListMetricsResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetMetricAlertsPathParameters = {
metricName: string;
};
export type GetMetricAlerts200 = {
data?: MetricsexplorertypesMetricAlertsResponseDTO;
data: MetricsexplorertypesMetricAlertsResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetMetricAttributesPathParameters = {
@@ -3123,117 +3257,117 @@ export type GetMetricAttributesParams = {
};
export type GetMetricAttributes200 = {
data?: MetricsexplorertypesMetricAttributesResponseDTO;
data: MetricsexplorertypesMetricAttributesResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetMetricDashboardsPathParameters = {
metricName: string;
};
export type GetMetricDashboards200 = {
data?: MetricsexplorertypesMetricDashboardsResponseDTO;
data: MetricsexplorertypesMetricDashboardsResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetMetricHighlightsPathParameters = {
metricName: string;
};
export type GetMetricHighlights200 = {
data?: MetricsexplorertypesMetricHighlightsResponseDTO;
data: MetricsexplorertypesMetricHighlightsResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetMetricMetadataPathParameters = {
metricName: string;
};
export type GetMetricMetadata200 = {
data?: MetricsexplorertypesMetricMetadataDTO;
data: MetricsexplorertypesMetricMetadataDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type UpdateMetricMetadataPathParameters = {
metricName: string;
};
export type GetMetricsStats200 = {
data?: MetricsexplorertypesStatsResponseDTO;
data: MetricsexplorertypesStatsResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetMetricsTreemap200 = {
data?: MetricsexplorertypesTreemapResponseDTO;
data: MetricsexplorertypesTreemapResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetMyOrganization200 = {
data?: TypesOrganizationDTO;
data: TypesOrganizationDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetSessionContext200 = {
data?: AuthtypesSessionContextDTO;
data: AuthtypesSessionContextDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type CreateSessionByEmailPassword200 = {
data?: AuthtypesGettableTokenDTO;
data: AuthtypesGettableTokenDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type RotateSession200 = {
data?: AuthtypesGettableTokenDTO;
data: AuthtypesGettableTokenDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type GetHosts200 = {
data?: ZeustypesGettableHostDTO;
data: ZeustypesGettableHostDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type QueryRangeV5200 = {
data?: Querybuildertypesv5QueryRangeResponseDTO;
data: Querybuildertypesv5QueryRangeResponseDTO;
/**
* @type string
*/
status?: string;
status: string;
};
export type ReplaceVariables200 = {
data?: Querybuildertypesv5QueryRangeRequestDTO;
data: Querybuildertypesv5QueryRangeRequestDTO;
/**
* @type string
*/
status?: string;
status: string;
};

View File

@@ -15,15 +15,7 @@ import { Events } from 'constants/events';
import { LOCALSTORAGE } from 'constants/localStorage';
import { eventEmitter } from 'utils/getEventEmitter';
import apiV1, {
apiAlertManager,
apiV2,
apiV3,
apiV4,
apiV5,
gatewayApiV1,
gatewayApiV2,
} from './apiV1';
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4, apiV5 } from './apiV1';
import { Logout } from './utils';
const RESPONSE_TIMEOUT_THRESHOLD = 5000; // 5 seconds
@@ -211,24 +203,6 @@ LogEventAxiosInstance.interceptors.response.use(
LogEventAxiosInstance.interceptors.request.use(interceptorsRequestResponse);
//
// gateway Api V1
export const GatewayApiV1Instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV1}`,
});
GatewayApiV1Instance.interceptors.response.use(
interceptorsResponse,
interceptorRejected,
);
GatewayApiV1Instance.interceptors.request.use(interceptorsRequestResponse);
//
// gateway Api V2
export const GatewayApiV2Instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV2}`,
});
// generated API Instance
export const GeneratedAPIInstance = axios.create({
baseURL: ENVIRONMENT.baseURL,
@@ -240,14 +214,6 @@ GeneratedAPIInstance.interceptors.response.use(
interceptorRejected,
);
GatewayApiV2Instance.interceptors.response.use(
interceptorsResponse,
interceptorRejected,
);
GatewayApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
//
AxiosAlertManagerInstance.interceptors.response.use(
interceptorsResponse,
interceptorRejected,

View File

@@ -1,20 +0,0 @@
import { GatewayApiV2Instance } from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { UpdateProfileProps } from 'types/api/onboarding/types';
const updateProfile = async (
props: UpdateProfileProps,
): Promise<SuccessResponse<UpdateProfileProps> | ErrorResponse> => {
const response = await GatewayApiV2Instance.put('/profiles/me', {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateProfile;

View File

@@ -40,6 +40,7 @@ function ValueGraph({
value,
rawValue,
thresholds,
yAxisUnit,
}: ValueGraphProps): JSX.Element {
const { t } = useTranslation(['valueGraph']);
const containerRef = useRef<HTMLDivElement>(null);
@@ -87,7 +88,7 @@ function ValueGraph({
const {
threshold,
isConflictingThresholds,
} = getBackgroundColorAndThresholdCheck(thresholds, rawValue);
} = getBackgroundColorAndThresholdCheck(thresholds, rawValue, yAxisUnit);
return (
<div
@@ -155,6 +156,7 @@ interface ValueGraphProps {
value: string;
rawValue: number;
thresholds: ThresholdProps[];
yAxisUnit?: string;
}
export default ValueGraph;

View File

@@ -1,9 +1,10 @@
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { evaluateThresholdWithConvertedValue } from 'container/GridTableComponent/utils';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
function compareThreshold(
function doesValueSatisfyThreshold(
rawValue: number,
threshold: ThresholdProps,
yAxisUnit?: string,
): boolean {
if (
threshold.thresholdOperator === undefined ||
@@ -11,31 +12,14 @@ function compareThreshold(
) {
return false;
}
switch (threshold.thresholdOperator) {
case '>':
return rawValue > threshold.thresholdValue;
case '>=':
return rawValue >= threshold.thresholdValue;
case '<':
return rawValue < threshold.thresholdValue;
case '<=':
return rawValue <= threshold.thresholdValue;
case '=':
return rawValue === threshold.thresholdValue;
default:
return false;
}
}
function extractNumbersFromString(inputString: string): number[] {
const regex = /[+-]?\d+(\.\d+)?/g;
const matches = inputString.match(regex);
if (matches) {
return matches.map(Number);
}
return [];
return evaluateThresholdWithConvertedValue(
rawValue,
threshold.thresholdValue,
threshold.thresholdOperator,
threshold.thresholdUnit,
yAxisUnit,
);
}
function getHighestPrecedenceThreshold(
@@ -63,17 +47,13 @@ function getHighestPrecedenceThreshold(
export function getBackgroundColorAndThresholdCheck(
thresholds: ThresholdProps[],
rawValue: number,
yAxisUnit?: string,
): {
threshold: ThresholdProps;
isConflictingThresholds: boolean;
} {
const matchingThresholds = thresholds.filter((threshold) =>
compareThreshold(
extractNumbersFromString(
getYAxisFormattedValue(rawValue.toString(), threshold.thresholdUnit || ''),
)[0],
threshold,
),
doesValueSatisfyThreshold(rawValue, threshold, yAxisUnit),
);
if (matchingThresholds.length === 0) {

View File

@@ -1,4 +1,5 @@
import { UniversalYAxisUnit } from '../types';
import { YAxisCategoryNames } from '../constants';
import { UniversalYAxisUnit, YAxisCategory } from '../types';
import {
getUniversalNameFromMetricUnit,
mapMetricUnitToUniversalUnit,
@@ -41,29 +42,29 @@ describe('YAxisUnitSelector utils', () => {
describe('mergeCategories', () => {
it('merges categories correctly', () => {
const categories1 = [
const categories1: YAxisCategory[] = [
{
name: 'Data',
name: YAxisCategoryNames.Data,
units: [
{ name: 'bytes', id: UniversalYAxisUnit.BYTES },
{ name: 'kilobytes', id: UniversalYAxisUnit.KILOBYTES },
],
},
];
const categories2 = [
const categories2: YAxisCategory[] = [
{
name: 'Data',
name: YAxisCategoryNames.Data,
units: [{ name: 'bits', id: UniversalYAxisUnit.BITS }],
},
{
name: 'Time',
name: YAxisCategoryNames.Time,
units: [{ name: 'seconds', id: UniversalYAxisUnit.SECONDS }],
},
];
const mergedCategories = mergeCategories(categories1, categories2);
expect(mergedCategories).toEqual([
{
name: 'Data',
name: YAxisCategoryNames.Data,
units: [
{ name: 'bytes', id: UniversalYAxisUnit.BYTES },
{ name: 'kilobytes', id: UniversalYAxisUnit.KILOBYTES },
@@ -71,7 +72,7 @@ describe('YAxisUnitSelector utils', () => {
],
},
{
name: 'Time',
name: YAxisCategoryNames.Time,
units: [{ name: 'seconds', id: UniversalYAxisUnit.SECONDS }],
},
]);

View File

@@ -1,5 +1,36 @@
import { UnitFamilyConfig, UniversalYAxisUnit, YAxisUnit } from './types';
export enum YAxisCategoryNames {
Time = 'Time',
Data = 'Data',
DataRate = 'Data Rate',
Count = 'Count',
Operations = 'Operations',
Percentage = 'Percentage',
Boolean = 'Boolean',
None = 'None',
HashRate = 'Hash Rate',
Miscellaneous = 'Miscellaneous',
Acceleration = 'Acceleration',
Angular = 'Angular',
Area = 'Area',
Flops = 'FLOPs',
Concentration = 'Concentration',
Currency = 'Currency',
Datetime = 'Datetime',
PowerElectrical = 'Power/Electrical',
Flow = 'Flow',
Force = 'Force',
Mass = 'Mass',
Length = 'Length',
Pressure = 'Pressure',
Radiation = 'Radiation',
RotationSpeed = 'Rotation Speed',
Temperature = 'Temperature',
Velocity = 'Velocity',
Volume = 'Volume',
}
// Mapping of universal y-axis units to their AWS, UCUM, and OpenMetrics equivalents (if available)
export const UniversalYAxisUnitMappings: Partial<
Record<UniversalYAxisUnit, Set<YAxisUnit> | null>

View File

@@ -1,10 +1,11 @@
import { Y_AXIS_UNIT_NAMES } from './constants';
import { YAxisCategoryNames } from './constants';
import { UniversalYAxisUnit, YAxisCategory } from './types';
// Base categories for the universal y-axis units
export const BASE_Y_AXIS_CATEGORIES: YAxisCategory[] = [
{
name: 'Time',
name: YAxisCategoryNames.Time,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.SECONDS],
@@ -37,7 +38,7 @@ export const BASE_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Data',
name: YAxisCategoryNames.Data,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.BYTES],
@@ -154,7 +155,7 @@ export const BASE_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Data Rate',
name: YAxisCategoryNames.DataRate,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.BYTES_SECOND],
@@ -295,7 +296,7 @@ export const BASE_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Count',
name: YAxisCategoryNames.Count,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.COUNT],
@@ -312,7 +313,7 @@ export const BASE_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Operations',
name: YAxisCategoryNames.Operations,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.OPS_SECOND],
@@ -353,7 +354,7 @@ export const BASE_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Percentage',
name: YAxisCategoryNames.Percentage,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.PERCENT],
@@ -366,7 +367,7 @@ export const BASE_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Boolean',
name: YAxisCategoryNames.Boolean,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.TRUE_FALSE],
@@ -382,7 +383,7 @@ export const BASE_Y_AXIS_CATEGORIES: YAxisCategory[] = [
export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
{
name: 'Time',
name: YAxisCategoryNames.Time,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.DURATION_MS],
@@ -419,7 +420,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Data Rate',
name: YAxisCategoryNames.DataRate,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.DATA_RATE_PACKETS_PER_SECOND],
@@ -428,7 +429,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Boolean',
name: YAxisCategoryNames.Boolean,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.ON_OFF],
@@ -437,7 +438,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'None',
name: YAxisCategoryNames.None,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.NONE],
@@ -446,7 +447,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Hash Rate',
name: YAxisCategoryNames.HashRate,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.HASH_RATE_HASHES_PER_SECOND],
@@ -479,7 +480,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Miscellaneous',
name: YAxisCategoryNames.Miscellaneous,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MISC_STRING],
@@ -520,7 +521,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Acceleration',
name: YAxisCategoryNames.Acceleration,
units: [
{
name:
@@ -541,7 +542,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Angular',
name: YAxisCategoryNames.Angular,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.ANGULAR_DEGREE],
@@ -566,7 +567,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Area',
name: YAxisCategoryNames.Area,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.AREA_SQUARE_METERS],
@@ -583,7 +584,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'FLOPs',
name: YAxisCategoryNames.Flops,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.FLOPS_FLOPS],
@@ -620,7 +621,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Concentration',
name: YAxisCategoryNames.Concentration,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.CONCENTRATION_PPM],
@@ -677,7 +678,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Currency',
name: YAxisCategoryNames.Currency,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.CURRENCY_USD],
@@ -774,7 +775,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Datetime',
name: YAxisCategoryNames.Datetime,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.DATETIME_ISO],
@@ -811,7 +812,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Power/Electrical',
name: YAxisCategoryNames.PowerElectrical,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.POWER_WATT],
@@ -968,7 +969,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Flow',
name: YAxisCategoryNames.Flow,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.FLOW_GALLONS_PER_MINUTE],
@@ -1005,7 +1006,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Force',
name: YAxisCategoryNames.Force,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.FORCE_NEWTON_METERS],
@@ -1026,7 +1027,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Mass',
name: YAxisCategoryNames.Mass,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MASS_MILLIGRAM],
@@ -1051,7 +1052,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Length',
name: YAxisCategoryNames.Length,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.LENGTH_MILLIMETER],
@@ -1080,7 +1081,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Pressure',
name: YAxisCategoryNames.Pressure,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.PRESSURE_MILLIBAR],
@@ -1117,7 +1118,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Radiation',
name: YAxisCategoryNames.Radiation,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.RADIATION_BECQUEREL],
@@ -1174,7 +1175,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Rotation Speed',
name: YAxisCategoryNames.RotationSpeed,
units: [
{
name:
@@ -1200,7 +1201,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Temperature',
name: YAxisCategoryNames.Temperature,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.TEMPERATURE_CELSIUS],
@@ -1217,7 +1218,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Velocity',
name: YAxisCategoryNames.Velocity,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.VELOCITY_METERS_PER_SECOND],
@@ -1238,7 +1239,7 @@ export const ADDITIONAL_Y_AXIS_CATEGORIES: YAxisCategory[] = [
],
},
{
name: 'Volume',
name: YAxisCategoryNames.Volume,
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.VOLUME_MILLILITER],

View File

@@ -1,3 +1,5 @@
import { YAxisCategoryNames } from './constants';
export interface YAxisUnitSelectorProps {
value: string | undefined;
onChange: (value: UniversalYAxisUnit) => void;
@@ -669,7 +671,7 @@ export interface UnitFamilyConfig {
}
export interface YAxisCategory {
name: string;
name: YAxisCategoryNames;
units: {
name: string;
id: UniversalYAxisUnit;

View File

@@ -202,7 +202,7 @@ function AllEndPoints({
const onRowClick = useCallback(
(props: any): void => {
setSelectedEndPointName(props[SPAN_ATTRIBUTES.URL_PATH] as string);
setSelectedEndPointName(props[SPAN_ATTRIBUTES.HTTP_URL] as string);
setSelectedView(VIEWS.ENDPOINT_STATS);
const initialItems = [
...(filters?.items || []),
@@ -213,7 +213,7 @@ function AllEndPoints({
op: 'AND',
});
setParams({
selectedEndPointName: props[SPAN_ATTRIBUTES.URL_PATH] as string,
selectedEndPointName: props[SPAN_ATTRIBUTES.HTTP_URL] as string,
selectedView: VIEWS.ENDPOINT_STATS,
endPointDetailsLocalFilters: {
items: initialItems,

View File

@@ -33,7 +33,7 @@ import { SPAN_ATTRIBUTES } from './constants';
const httpUrlKey = {
dataType: DataTypes.String,
key: SPAN_ATTRIBUTES.URL_PATH,
key: SPAN_ATTRIBUTES.HTTP_URL,
type: 'tag',
};
@@ -93,7 +93,7 @@ function EndPointDetails({
return currentFilters; // No change needed, prevents loop
}
// Rebuild filters: Keep non-http.url filters and add/update http.url filter based on prop
// Rebuild filters: Keep non-http_url filters and add/update http_url filter based on prop
const otherFilters = currentFilters?.items?.filter(
(item) => item.key?.key !== httpUrlKey.key,
);
@@ -125,7 +125,7 @@ function EndPointDetails({
(newFilters: IBuilderQuery['filters']): void => {
// 1. Update local filters state immediately
setFilters(newFilters);
// Filter out http.url filter before saving to params
// Filter out http_url filter before saving to params
const filteredNewFilters = {
op: 'AND',
items:
@@ -299,7 +299,6 @@ function EndPointDetails({
endPointStatusCodeLatencyBarChartsDataQuery
}
domainName={domainName}
endPointName={endPointName}
filters={filters}
timeRange={timeRange}
onDragSelect={onDragSelect}

View File

@@ -56,15 +56,15 @@ function TopErrors({
{
items: endPointName
? [
// Remove any existing http.url filters from initialFilters to avoid duplicates
// Remove any existing http_url filters from initialFilters to avoid duplicates
...(initialFilters?.items?.filter(
(item) => item.key?.key !== SPAN_ATTRIBUTES.URL_PATH,
(item) => item.key?.key !== SPAN_ATTRIBUTES.HTTP_URL,
) || []),
{
id: '92b8a1c1',
key: {
dataType: DataTypes.String,
key: SPAN_ATTRIBUTES.URL_PATH,
key: SPAN_ATTRIBUTES.HTTP_URL,
type: 'tag',
},
op: '=',

View File

@@ -9,6 +9,7 @@ import { ENTITY_VERSION_V5 } from 'constants/app';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { SPAN_ATTRIBUTES } from '../constants';
import DomainMetrics from './DomainMetrics';
// Mock the API call
@@ -126,11 +127,9 @@ describe('DomainMetrics - V5 Query Payload Tests', () => {
'count()',
);
// Verify exact domain filter expression structure
expect(queryA.filter.expression).toContain("http_host = '0.0.0.0'");
expect(queryA.filter.expression).toContain(
"(net.peer.name = '0.0.0.0' OR server.address = '0.0.0.0')",
);
expect(queryA.filter.expression).toContain(
'url.full EXISTS OR http.url EXISTS',
`${SPAN_ATTRIBUTES.HTTP_URL} EXISTS`,
);
// Verify Query B - p99 latency
@@ -142,17 +141,13 @@ describe('DomainMetrics - V5 Query Payload Tests', () => {
'p99(duration_nano)',
);
// Verify exact domain filter expression structure
expect(queryB.filter.expression).toContain(
"(net.peer.name = '0.0.0.0' OR server.address = '0.0.0.0')",
);
expect(queryB.filter.expression).toContain("http_host = '0.0.0.0'");
// Verify Query C - error count (disabled)
const queryC = queryData.find((q: any) => q.queryName === 'C');
expect(queryC).toBeDefined();
expect(queryC.disabled).toBe(true);
expect(queryC.filter.expression).toContain(
"(net.peer.name = '0.0.0.0' OR server.address = '0.0.0.0')",
);
expect(queryC.filter.expression).toContain("http_host = '0.0.0.0'");
expect(queryC.aggregations?.[0]).toBeDefined();
expect((queryC.aggregations?.[0] as TraceAggregation)?.expression).toBe(
'count()',
@@ -169,9 +164,7 @@ describe('DomainMetrics - V5 Query Payload Tests', () => {
'max(timestamp)',
);
// Verify exact domain filter expression structure
expect(queryD.filter.expression).toContain(
"(net.peer.name = '0.0.0.0' OR server.address = '0.0.0.0')",
);
expect(queryD.filter.expression).toContain("http_host = '0.0.0.0'");
// Verify Formula F1 - error rate calculation
const formulas = payload.query.builder.queryFormulas;

View File

@@ -153,7 +153,7 @@ describe('EndPointMetrics - V5 Query Payload Tests', () => {
// Verify exact domain filter expression structure
if (queryA.filter) {
expect(queryA.filter.expression).toContain(
"(net.peer.name = 'api.example.com' OR server.address = 'api.example.com')",
`http_host = 'api.example.com'`,
);
expect(queryA.filter.expression).toContain("kind_string = 'Client'");
}
@@ -171,7 +171,7 @@ describe('EndPointMetrics - V5 Query Payload Tests', () => {
// Verify exact domain filter expression structure
if (queryB.filter) {
expect(queryB.filter.expression).toContain(
"(net.peer.name = 'api.example.com' OR server.address = 'api.example.com')",
`http_host = 'api.example.com'`,
);
expect(queryB.filter.expression).toContain("kind_string = 'Client'");
}
@@ -185,7 +185,7 @@ describe('EndPointMetrics - V5 Query Payload Tests', () => {
expect(queryC.aggregateOperator).toBe('count');
if (queryC.filter) {
expect(queryC.filter.expression).toContain(
"(net.peer.name = 'api.example.com' OR server.address = 'api.example.com')",
`http_host = 'api.example.com'`,
);
expect(queryC.filter.expression).toContain("kind_string = 'Client'");
expect(queryC.filter.expression).toContain('has_error = true');
@@ -204,7 +204,7 @@ describe('EndPointMetrics - V5 Query Payload Tests', () => {
// Verify exact domain filter expression structure
if (queryD.filter) {
expect(queryD.filter.expression).toContain(
"(net.peer.name = 'api.example.com' OR server.address = 'api.example.com')",
`http_host = 'api.example.com'`,
);
expect(queryD.filter.expression).toContain("kind_string = 'Client'");
}
@@ -221,7 +221,7 @@ describe('EndPointMetrics - V5 Query Payload Tests', () => {
}
if (queryE.filter) {
expect(queryE.filter.expression).toContain(
"(net.peer.name = 'api.example.com' OR server.address = 'api.example.com')",
`http_host = 'api.example.com'`,
);
expect(queryE.filter.expression).toContain("kind_string = 'Client'");
}
@@ -291,7 +291,7 @@ describe('EndPointMetrics - V5 Query Payload Tests', () => {
expect(query.filter.expression).toContain('staging');
// Also verify domain filter is still present
expect(query.filter.expression).toContain(
"(net.peer.name = 'api.internal.com' OR server.address = 'api.internal.com')",
"http_host = 'api.internal.com'",
);
// Verify client kind filter is present
expect(query.filter.expression).toContain("kind_string = 'Client'");

View File

@@ -34,7 +34,6 @@ function StatusCodeBarCharts({
endPointStatusCodeBarChartsDataQuery,
endPointStatusCodeLatencyBarChartsDataQuery,
domainName,
endPointName,
filters,
timeRange,
onDragSelect,
@@ -48,7 +47,6 @@ function StatusCodeBarCharts({
unknown
>;
domainName: string;
endPointName: string;
filters: IBuilderQuery['filters'];
timeRange: {
startTime: number;
@@ -144,11 +142,11 @@ function StatusCodeBarCharts({
const widget = useMemo<Widgets>(
() =>
getStatusCodeBarChartWidgetData(domainName, endPointName, {
getStatusCodeBarChartWidgetData(domainName, {
items: [...(filters?.items || [])],
op: filters?.op || 'AND',
}),
[domainName, endPointName, filters],
[domainName, filters],
);
const graphClickHandler = useCallback(
@@ -166,6 +164,7 @@ function StatusCodeBarCharts({
xValue,
TWO_AND_HALF_MINUTES_IN_MILLISECONDS,
);
handleGraphClick({
xValue,
yValue,

View File

@@ -12,7 +12,7 @@ export const VIEW_TYPES = {
// Span attribute keys - these are the source of truth for all attribute keys
export const SPAN_ATTRIBUTES = {
URL_PATH: 'http.url',
HTTP_URL: 'http_url',
RESPONSE_STATUS_CODE: 'response_status_code',
SERVER_NAME: 'http_host',
SERVER_PORT: 'net.peer.port',

View File

@@ -280,7 +280,7 @@ describe('API Monitoring Utils', () => {
const endpointFilter = result?.items?.find(
(item) =>
item.key &&
item.key.key === SPAN_ATTRIBUTES.URL_PATH &&
item.key.key === SPAN_ATTRIBUTES.HTTP_URL &&
item.value === endPointName,
);
expect(endpointFilter).toBeDefined();
@@ -344,13 +344,12 @@ describe('API Monitoring Utils', () => {
describe('getFormattedEndPointDropDownData', () => {
it('should format endpoint dropdown data correctly', () => {
// Arrange
const URL_PATH_KEY = SPAN_ATTRIBUTES.URL_PATH;
const URL_PATH_KEY = SPAN_ATTRIBUTES.HTTP_URL;
const mockData = [
{
data: {
// eslint-disable-next-line sonarjs/no-duplicate-string
[URL_PATH_KEY]: '/api/users',
'url.full': 'http://example.com/api/users',
A: 150, // count or other metric
},
},
@@ -358,7 +357,6 @@ describe('API Monitoring Utils', () => {
data: {
// eslint-disable-next-line sonarjs/no-duplicate-string
[URL_PATH_KEY]: '/api/orders',
'url.full': 'http://example.com/api/orders',
A: 75,
},
},
@@ -406,7 +404,7 @@ describe('API Monitoring Utils', () => {
it('should handle items without URL path', () => {
// Arrange
const URL_PATH_KEY = SPAN_ATTRIBUTES.URL_PATH;
const URL_PATH_KEY = SPAN_ATTRIBUTES.HTTP_URL;
type MockDataType = {
data: {
[key: string]: string | number;
@@ -712,13 +710,11 @@ describe('API Monitoring Utils', () => {
it('should generate widget configuration for status code bar chart', () => {
// Arrange
const domainName = 'test-domain';
const endPointName = '/api/test';
const filters = { items: [], op: 'AND' };
// Act
const result = getStatusCodeBarChartWidgetData(
domainName,
endPointName,
filters as IBuilderQuery['filters'],
);
@@ -741,21 +737,11 @@ describe('API Monitoring Utils', () => {
if (domainFilter) {
expect(domainFilter.value).toBe(domainName);
}
// Should have endpoint filter if provided
const endpointFilter = queryData.filters?.items?.find(
(item) => item.key && item.key.key === SPAN_ATTRIBUTES.URL_PATH,
);
expect(endpointFilter).toBeDefined();
if (endpointFilter) {
expect(endpointFilter.value).toBe(endPointName);
}
});
it('should include custom filters in the widget configuration', () => {
// Arrange
const domainName = 'test-domain';
const endPointName = '/api/test';
const customFilter = {
id: 'custom-filter',
key: {
@@ -771,7 +757,6 @@ describe('API Monitoring Utils', () => {
// Act
const result = getStatusCodeBarChartWidgetData(
domainName,
endPointName,
filters as IBuilderQuery['filters'],
);

View File

@@ -25,7 +25,7 @@ jest.mock('container/GridCardLayout/GridCard', () => ({
type="button"
data-testid="row-click-button"
onClick={(): void =>
customOnRowClick({ [SPAN_ATTRIBUTES.URL_PATH]: '/api/test' })
customOnRowClick({ [SPAN_ATTRIBUTES.HTTP_URL]: '/api/test' })
}
>
Click Row

View File

@@ -6,10 +6,10 @@
* These tests validate the migration from V4 to V5 format for getAllEndpointsWidgetData:
* - Filter format change: filters.items[] → filter.expression
* - Aggregation format: aggregateAttribute → aggregations[] array
* - Domain filter: (net.peer.name OR server.address)
* - Domain filter: http_host = '${domainName}'
* - Kind filter: kind_string = 'Client'
* - Four queries: A (count), B (p99 latency), C (max timestamp), D (error count - disabled)
* - GroupBy: Both http.url AND url.full with type 'attribute'
* - GroupBy: http_url with type 'attribute'
*/
import { getAllEndpointsWidgetData } from 'container/ApiMonitoring/utils';
import {
@@ -18,6 +18,8 @@ import {
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { SPAN_ATTRIBUTES } from '../Explorer/Domains/DomainDetails/constants';
describe('AllEndpointsWidget - V5 Migration Validation', () => {
const mockDomainName = 'api.example.com';
const emptyFilters: IBuilderQuery['filters'] = {
@@ -92,28 +94,28 @@ describe('AllEndpointsWidget - V5 Migration Validation', () => {
const [queryA, queryB, queryC, queryD] = widget.query.builder.queryData;
const baseExpression = `(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}') AND kind_string = 'Client'`;
const baseExpression = `http_host = '${mockDomainName}' AND kind_string = 'Client'`;
// Queries A, B, C have identical base filter
expect(queryA.filter?.expression).toBe(
`${baseExpression} AND (http.url EXISTS OR url.full EXISTS)`,
`${baseExpression} AND ${SPAN_ATTRIBUTES.HTTP_URL} EXISTS`,
);
expect(queryB.filter?.expression).toBe(
`${baseExpression} AND (http.url EXISTS OR url.full EXISTS)`,
`${baseExpression} AND ${SPAN_ATTRIBUTES.HTTP_URL} EXISTS`,
);
expect(queryC.filter?.expression).toBe(
`${baseExpression} AND (http.url EXISTS OR url.full EXISTS)`,
`${baseExpression} AND ${SPAN_ATTRIBUTES.HTTP_URL} EXISTS`,
);
// Query D has additional has_error filter
expect(queryD.filter?.expression).toBe(
`${baseExpression} AND has_error = true AND (http.url EXISTS OR url.full EXISTS)`,
`${baseExpression} AND has_error = true AND ${SPAN_ATTRIBUTES.HTTP_URL} EXISTS`,
);
});
});
describe('2. GroupBy Structure', () => {
it('default groupBy includes both http.url and url.full with type attribute', () => {
it(`default groupBy includes ${SPAN_ATTRIBUTES.HTTP_URL} with type attribute`, () => {
const widget = getAllEndpointsWidgetData(
emptyGroupBy,
mockDomainName,
@@ -124,23 +126,13 @@ describe('AllEndpointsWidget - V5 Migration Validation', () => {
// All queries should have the same default groupBy
queryData.forEach((query) => {
expect(query.groupBy).toHaveLength(2);
expect(query.groupBy).toHaveLength(1);
// http.url
expect(query.groupBy).toContainEqual({
dataType: DataTypes.String,
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'attribute',
});
// url.full
expect(query.groupBy).toContainEqual({
dataType: DataTypes.String,
isColumn: false,
isJSON: false,
key: 'url.full',
key: SPAN_ATTRIBUTES.HTTP_URL,
type: 'attribute',
});
});
@@ -170,19 +162,18 @@ describe('AllEndpointsWidget - V5 Migration Validation', () => {
// All queries should have defaults + custom groupBy
queryData.forEach((query) => {
expect(query.groupBy).toHaveLength(4); // 2 defaults + 2 custom
expect(query.groupBy).toHaveLength(3); // 1 default + 2 custom
// First two should be defaults (http.url, url.full)
expect(query.groupBy[0].key).toBe('http.url');
expect(query.groupBy[1].key).toBe('url.full');
// First two should be defaults (http_url)
expect(query.groupBy[0].key).toBe(SPAN_ATTRIBUTES.HTTP_URL);
// Last two should be custom (matching subset of properties)
expect(query.groupBy[2]).toMatchObject({
expect(query.groupBy[1]).toMatchObject({
dataType: DataTypes.String,
key: 'service.name',
type: 'resource',
});
expect(query.groupBy[3]).toMatchObject({
expect(query.groupBy[2]).toMatchObject({
dataType: DataTypes.String,
key: 'deployment.environment',
type: 'resource',

View File

@@ -258,7 +258,7 @@ describe('EndPointDetails Component', () => {
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.URL_PATH }),
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.HTTP_URL }),
value: '/api/test',
}),
]),
@@ -278,7 +278,7 @@ describe('EndPointDetails Component', () => {
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.URL_PATH }),
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.HTTP_URL }),
value: '/api/test',
}),
]),
@@ -360,7 +360,7 @@ describe('EndPointDetails Component', () => {
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.URL_PATH }),
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.HTTP_URL }),
value: '/api/test',
}),
]),
@@ -373,7 +373,7 @@ describe('EndPointDetails Component', () => {
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.URL_PATH }),
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.HTTP_URL }),
value: '/api/test',
}),
]),

View File

@@ -191,7 +191,7 @@ describe('EndPointsDropDown Component', () => {
it('formats data using the utility function', () => {
const mockRows = [
{ data: { [SPAN_ATTRIBUTES.URL_PATH]: '/api/test', A: 10 } },
{ data: { [SPAN_ATTRIBUTES.HTTP_URL]: '/api/test', A: 10 } },
];
const dataProps = {

View File

@@ -6,15 +6,18 @@
* These tests validate the migration from V4 to V5 format for the third payload
* in getEndPointDetailsQueryPayload (endpoint dropdown data):
* - Filter format change: filters.items[] → filter.expression
* - Domain handling: (net.peer.name OR server.address)
* - Domain handling: http_host = '${domainName}'
* - Kind filter: kind_string = 'Client'
* - Existence check: (http.url EXISTS OR url.full EXISTS)
* - Existence check: http_url EXISTS
* - Aggregation: count() expression
* - GroupBy: Both http.url AND url.full with type 'attribute'
* - GroupBy: http_url with type 'attribute'
*/
import { getEndPointDetailsQueryPayload } from 'container/ApiMonitoring/utils';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { SPAN_ATTRIBUTES } from '../Explorer/Domains/DomainDetails/constants';
describe('EndpointDropdown - V5 Migration Validation', () => {
const mockDomainName = 'api.example.com';
const mockStartTime = 1000;
@@ -43,9 +46,9 @@ describe('EndpointDropdown - V5 Migration Validation', () => {
expect(typeof queryA.filter?.expression).toBe('string');
expect(queryA).not.toHaveProperty('filters');
// Base filter 1: Domain (net.peer.name OR server.address)
// Base filter 1: Domain http_host = '${domainName}'
expect(queryA.filter?.expression).toContain(
`(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}')`,
`http_host = '${mockDomainName}'`,
);
// Base filter 2: Kind
@@ -53,7 +56,7 @@ describe('EndpointDropdown - V5 Migration Validation', () => {
// Base filter 3: Existence check
expect(queryA.filter?.expression).toContain(
'(http.url EXISTS OR url.full EXISTS)',
`${SPAN_ATTRIBUTES.HTTP_URL} EXISTS`,
);
// V5 Aggregation format: aggregations array (not aggregateAttribute)
@@ -64,16 +67,11 @@ describe('EndpointDropdown - V5 Migration Validation', () => {
});
expect(queryA).not.toHaveProperty('aggregateAttribute');
// GroupBy: Both http.url and url.full
expect(queryA.groupBy).toHaveLength(2);
// GroupBy: http_url
expect(queryA.groupBy).toHaveLength(1);
expect(queryA.groupBy).toContainEqual({
key: 'http.url',
dataType: 'string',
type: 'attribute',
});
expect(queryA.groupBy).toContainEqual({
key: 'url.full',
dataType: 'string',
key: SPAN_ATTRIBUTES.HTTP_URL,
dataType: DataTypes.String,
type: 'attribute',
});
});
@@ -120,53 +118,7 @@ describe('EndpointDropdown - V5 Migration Validation', () => {
// Exact filter expression with custom filters merged
expect(expression).toBe(
"(net.peer.name = 'api.example.com' OR server.address = 'api.example.com') AND kind_string = 'Client' AND (http.url EXISTS OR url.full EXISTS) service.name = 'user-service' AND deployment.environment = 'production'",
);
});
});
describe('3. HTTP URL Filter Special Handling', () => {
it('converts http.url filter to (http.url OR url.full) expression', () => {
const filtersWithHttpUrl: IBuilderQuery['filters'] = {
items: [
{
id: 'http-url-filter',
key: {
key: 'http.url',
dataType: 'string' as any,
type: 'tag',
},
op: '=',
value: '/api/users',
},
{
id: 'service-filter',
key: {
key: 'service.name',
dataType: 'string' as any,
type: 'resource',
},
op: '=',
value: 'user-service',
},
],
op: 'AND',
};
const payload = getEndPointDetailsQueryPayload(
mockDomainName,
mockStartTime,
mockEndTime,
filtersWithHttpUrl,
);
const dropdownQuery = payload[2];
const expression =
dropdownQuery.query.builder.queryData[0].filter?.expression;
// CRITICAL: Exact filter expression with http.url converted to OR logic
expect(expression).toBe(
"(net.peer.name = 'api.example.com' OR server.address = 'api.example.com') AND kind_string = 'Client' AND (http.url EXISTS OR url.full EXISTS) service.name = 'user-service' AND (http.url = '/api/users' OR url.full = '/api/users')",
`${SPAN_ATTRIBUTES.SERVER_NAME} = 'api.example.com' AND kind_string = 'Client' AND ${SPAN_ATTRIBUTES.HTTP_URL} EXISTS service.name = 'user-service' AND deployment.environment = 'production'`,
);
});
});

View File

@@ -33,7 +33,7 @@ describe('MetricOverTime - V5 Migration Validation', () => {
expect(queryData).not.toHaveProperty('filters.items');
});
it('uses new domain filter format: (net.peer.name OR server.address)', () => {
it('uses new domain filter format: (http_host)', () => {
const widget = getRateOverTimeWidgetData(
mockDomainName,
mockEndpointName,
@@ -44,7 +44,7 @@ describe('MetricOverTime - V5 Migration Validation', () => {
// Verify EXACT new filter format with OR operator
expect(queryData?.filter?.expression).toContain(
`(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}')`,
`http_host = '${mockDomainName}'`,
);
// Endpoint name is used in legend, not filter
@@ -90,7 +90,7 @@ describe('MetricOverTime - V5 Migration Validation', () => {
// Verify domain filter is present
expect(queryData?.filter?.expression).toContain(
`(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}')`,
`http_host = '${mockDomainName}'`,
);
// Verify custom filters are merged into the expression
@@ -120,7 +120,7 @@ describe('MetricOverTime - V5 Migration Validation', () => {
expect(queryData).not.toHaveProperty('filters.items');
});
it('uses new domain filter format: (net.peer.name OR server.address)', () => {
it('uses new domain filter format: (http_host)', () => {
const widget = getLatencyOverTimeWidgetData(
mockDomainName,
mockEndpointName,
@@ -132,7 +132,7 @@ describe('MetricOverTime - V5 Migration Validation', () => {
// Verify EXACT new filter format with OR operator
expect(queryData.filter).toBeDefined();
expect(queryData?.filter?.expression).toContain(
`(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}')`,
`http_host = '${mockDomainName}'`,
);
// Endpoint name is used in legend, not filter
@@ -166,7 +166,7 @@ describe('MetricOverTime - V5 Migration Validation', () => {
// Verify domain filter is present
expect(queryData?.filter?.expression).toContain(
`(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}') service.name = 'user-service'`,
`http_host = '${mockDomainName}' service.name = 'user-service'`,
);
});
});

View File

@@ -142,7 +142,6 @@ describe('StatusCodeBarCharts', () => {
endTime: 1609545600000,
};
const mockDomainName = 'test-domain';
const mockEndPointName = '/api/test';
const onDragSelectMock = jest.fn();
const refetchFn = jest.fn();
@@ -232,7 +231,6 @@ describe('StatusCodeBarCharts', () => {
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
domainName={mockDomainName}
endPointName={mockEndPointName}
filters={mockFilters}
timeRange={mockTimeRange}
onDragSelect={onDragSelectMock}
@@ -268,7 +266,6 @@ describe('StatusCodeBarCharts', () => {
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
domainName={mockDomainName}
endPointName={mockEndPointName}
filters={mockFilters}
timeRange={mockTimeRange}
onDragSelect={onDragSelectMock}
@@ -311,7 +308,6 @@ describe('StatusCodeBarCharts', () => {
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
domainName={mockDomainName}
endPointName={mockEndPointName}
filters={mockFilters}
timeRange={mockTimeRange}
onDragSelect={onDragSelectMock}
@@ -356,7 +352,6 @@ describe('StatusCodeBarCharts', () => {
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
domainName={mockDomainName}
endPointName={mockEndPointName}
filters={mockFilters}
timeRange={mockTimeRange}
onDragSelect={onDragSelectMock}
@@ -404,7 +399,6 @@ describe('StatusCodeBarCharts', () => {
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
domainName={mockDomainName}
endPointName={mockEndPointName}
filters={mockFilters}
timeRange={mockTimeRange}
onDragSelect={onDragSelectMock}
@@ -419,7 +413,6 @@ describe('StatusCodeBarCharts', () => {
// but we've confirmed the function is mocked and ready to be tested
expect(getStatusCodeBarChartWidgetData).toHaveBeenCalledWith(
mockDomainName,
mockEndPointName,
expect.objectContaining({
items: [],
op: 'AND',
@@ -467,7 +460,6 @@ describe('StatusCodeBarCharts', () => {
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
domainName={mockDomainName}
endPointName={mockEndPointName}
filters={mockCustomFilters as IBuilderQuery['filters']}
timeRange={mockTimeRange}
onDragSelect={onDragSelectMock}
@@ -477,7 +469,6 @@ describe('StatusCodeBarCharts', () => {
// Assert widget creation was called with the correct parameters
expect(getStatusCodeBarChartWidgetData).toHaveBeenCalledWith(
mockDomainName,
mockEndPointName,
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({ id: 'custom-filter' }),

View File

@@ -10,7 +10,7 @@
*
* V5 Changes:
* - Filter format change: filters.items[] → filter.expression
* - Domain filter: (net.peer.name OR server.address)
* - Domain filter: (http_host)
* - Kind filter: kind_string = 'Client'
* - stepInterval: 60 → null
* - Grouped by response_status_code
@@ -47,9 +47,9 @@ describe('StatusCodeBarCharts - V5 Migration Validation', () => {
expect(typeof queryA.filter?.expression).toBe('string');
expect(queryA).not.toHaveProperty('filters.items');
// Base filter 1: Domain (net.peer.name OR server.address)
// Base filter 1: Domain (http_host)
expect(queryA.filter?.expression).toContain(
`(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}')`,
`http_host = '${mockDomainName}'`,
);
// Base filter 2: Kind
@@ -96,9 +96,9 @@ describe('StatusCodeBarCharts - V5 Migration Validation', () => {
expect(typeof queryA.filter?.expression).toBe('string');
expect(queryA).not.toHaveProperty('filters.items');
// Base filter 1: Domain (net.peer.name OR server.address)
// Base filter 1: Domain (http_host)
expect(queryA.filter?.expression).toContain(
`(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}')`,
`http_host = '${mockDomainName}'`,
);
// Base filter 2: Kind
@@ -177,7 +177,7 @@ describe('StatusCodeBarCharts - V5 Migration Validation', () => {
expect(callsExpression).toBe(latencyExpression);
// Verify base filters
expect(callsExpression).toContain('net.peer.name');
expect(callsExpression).toContain('http_host');
expect(callsExpression).toContain("kind_string = 'Client'");
// Verify custom filters are merged
@@ -187,51 +187,4 @@ describe('StatusCodeBarCharts - V5 Migration Validation', () => {
expect(callsExpression).toContain('production');
});
});
describe('4. HTTP URL Filter Handling', () => {
it('converts http.url filter to (http.url OR url.full) expression in both charts', () => {
const filtersWithHttpUrl: IBuilderQuery['filters'] = {
items: [
{
id: 'http-url-filter',
key: {
key: 'http.url',
dataType: 'string' as any,
type: 'tag',
},
op: '=',
value: '/api/metrics',
},
],
op: 'AND',
};
const payload = getEndPointDetailsQueryPayload(
mockDomainName,
mockStartTime,
mockEndTime,
filtersWithHttpUrl,
);
const callsChartQuery = payload[4];
const latencyChartQuery = payload[5];
const callsExpression =
callsChartQuery.query.builder.queryData[0].filter?.expression;
const latencyExpression =
latencyChartQuery.query.builder.queryData[0].filter?.expression;
// CRITICAL: http.url converted to OR logic
expect(callsExpression).toContain(
"(http.url = '/api/metrics' OR url.full = '/api/metrics')",
);
expect(latencyExpression).toContain(
"(http.url = '/api/metrics' OR url.full = '/api/metrics')",
);
// Base filters still present
expect(callsExpression).toContain('net.peer.name');
expect(callsExpression).toContain("kind_string = 'Client'");
});
});
});

View File

@@ -6,8 +6,8 @@
* These tests validate the migration from V4 to V5 format for the second payload
* in getEndPointDetailsQueryPayload (status code table data):
* - Filter format change: filters.items[] → filter.expression
* - URL handling: Special logic for (http.url OR url.full)
* - Domain filter: (net.peer.name OR server.address)
* - URL handling: Special logic for http_url
* - Domain filter: http_host = '${domainName}'
* - Kind filter: kind_string = 'Client'
* - Kind filter: response_status_code EXISTS
* - Three queries: A (count), B (p99 latency), C (rate)
@@ -45,9 +45,9 @@ describe('StatusCodeTable - V5 Migration Validation', () => {
expect(typeof queryA.filter?.expression).toBe('string');
expect(queryA).not.toHaveProperty('filters.items');
// Base filter 1: Domain (net.peer.name OR server.address)
// Base filter 1: Domain (http_host)
expect(queryA.filter?.expression).toContain(
`(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}')`,
`http_host = '${mockDomainName}'`,
);
// Base filter 2: Kind
@@ -149,7 +149,7 @@ describe('StatusCodeTable - V5 Migration Validation', () => {
statusCodeQuery.query.builder.queryData[0].filter?.expression;
// Base filters present
expect(expression).toContain('net.peer.name');
expect(expression).toContain('http_host');
expect(expression).toContain("kind_string = 'Client'");
expect(expression).toContain('response_status_code EXISTS');
@@ -165,62 +165,4 @@ describe('StatusCodeTable - V5 Migration Validation', () => {
expect(queries[1].filter?.expression).toBe(queries[2].filter?.expression);
});
});
describe('4. HTTP URL Filter Handling', () => {
it('converts http.url filter to (http.url OR url.full) expression', () => {
const filtersWithHttpUrl: IBuilderQuery['filters'] = {
items: [
{
id: 'http-url-filter',
key: {
key: 'http.url',
dataType: 'string' as any,
type: 'tag',
},
op: '=',
value: '/api/users',
},
{
id: 'service-filter',
key: {
key: 'service.name',
dataType: 'string' as any,
type: 'resource',
},
op: '=',
value: 'user-service',
},
],
op: 'AND',
};
const payload = getEndPointDetailsQueryPayload(
mockDomainName,
mockStartTime,
mockEndTime,
filtersWithHttpUrl,
);
const statusCodeQuery = payload[1];
const expression =
statusCodeQuery.query.builder.queryData[0].filter?.expression;
// CRITICAL: http.url converted to OR logic
expect(expression).toContain(
"(http.url = '/api/users' OR url.full = '/api/users')",
);
// Other filters still present
expect(expression).toContain('service.name');
expect(expression).toContain('user-service');
// Base filters present
expect(expression).toContain('net.peer.name');
expect(expression).toContain("kind_string = 'Client'");
expect(expression).toContain('response_status_code EXISTS');
// All ANDed together (at least 2 ANDs: domain+kind, custom filter, url condition)
expect(expression?.match(/AND/g)?.length).toBeGreaterThanOrEqual(2);
});
});
});

View File

@@ -84,7 +84,7 @@ describe('TopErrors', () => {
{
columns: [
{
name: 'http.url',
name: SPAN_ATTRIBUTES.HTTP_URL,
fieldDataType: 'string',
fieldContext: 'attribute',
},
@@ -124,7 +124,7 @@ describe('TopErrors', () => {
table: {
rows: [
{
'http.url': '/api/test',
http_url: '/api/test',
A: 100,
},
],
@@ -206,7 +206,7 @@ describe('TopErrors', () => {
expect(navigateMock).toHaveBeenCalledWith({
filters: expect.arrayContaining([
expect.objectContaining({
key: expect.objectContaining({ key: 'http.url' }),
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.HTTP_URL }),
op: '=',
value: '/api/test',
}),
@@ -335,7 +335,7 @@ describe('TopErrors', () => {
// Verify all required filters are present
expect(filterExpression).toContain(
`kind_string = 'Client' AND (http.url EXISTS OR url.full EXISTS) AND (net.peer.name = 'test-domain' OR server.address = 'test-domain') AND has_error = true`,
`kind_string = 'Client' AND ${SPAN_ATTRIBUTES.HTTP_URL} EXISTS AND ${SPAN_ATTRIBUTES.SERVER_NAME} = 'test-domain' AND has_error = true`,
);
});
});

View File

@@ -15,7 +15,6 @@ import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsAppli
import { convertNanoToMilliseconds } from 'container/MetricsExplorer/Summary/utils';
import dayjs from 'dayjs';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { cloneDeep } from 'lodash-es';
import { ArrowUpDown, ChevronDown, ChevronRight, Info } from 'lucide-react';
import { getWidgetQuery } from 'pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil';
@@ -57,12 +56,12 @@ export const getDisplayValue = (value: unknown): string =>
isEmptyFilterValue(value) ? '-' : String(value);
export const getDomainNameFilterExpression = (domainName: string): string =>
`(net.peer.name = '${domainName}' OR server.address = '${domainName}')`;
`http_host = '${domainName}'`;
export const clientKindExpression = `kind_string = 'Client'`;
/**
* Converts filters to expression, handling http.url specially by creating (http.url OR url.full) condition
* Converts filters to expression
* @param filters Filters to convert
* @param baseExpression Base expression to combine with filters
* @returns Filter expression string
@@ -75,34 +74,6 @@ export const convertFiltersWithUrlHandling = (
return baseExpression;
}
// Check if filters contain http.url (SPAN_ATTRIBUTES.URL_PATH)
const httpUrlFilter = filters.items?.find(
(item) => item.key?.key === SPAN_ATTRIBUTES.URL_PATH,
);
// If http.url filter exists, create modified filters with (http.url OR url.full)
if (httpUrlFilter && httpUrlFilter.value) {
// Remove ALL http.url filters from items (guards against duplicates)
const otherFilters = filters.items?.filter(
(item) => item.key?.key !== SPAN_ATTRIBUTES.URL_PATH,
);
// Convert to expression first with other filters
const {
filter: intermediateFilter,
} = convertFiltersToExpressionWithExistingQuery(
{ ...filters, items: otherFilters || [] },
baseExpression,
);
// Add the OR condition for http.url and url.full
const urlValue = httpUrlFilter.value;
const urlCondition = `(http.url = '${urlValue}' OR url.full = '${urlValue}')`;
return intermediateFilter.expression.trim()
? `${intermediateFilter.expression} AND ${urlCondition}`
: urlCondition;
}
const { filter } = convertFiltersToExpressionWithExistingQuery(
filters,
baseExpression,
@@ -371,7 +342,7 @@ export const formatDataForTable = (
});
};
const urlExpression = `(url.full EXISTS OR http.url EXISTS)`;
const urlExpression = `${SPAN_ATTRIBUTES.HTTP_URL} EXISTS`;
export const getDomainMetricsQueryPayload = (
domainName: string,
@@ -588,14 +559,7 @@ const defaultGroupBy = [
dataType: DataTypes.String,
isColumn: false,
isJSON: false,
key: SPAN_ATTRIBUTES.URL_PATH,
type: 'attribute',
},
{
dataType: DataTypes.String,
isColumn: false,
isJSON: false,
key: 'url.full',
key: SPAN_ATTRIBUTES.HTTP_URL,
type: 'attribute',
},
// {
@@ -867,8 +831,8 @@ function buildFilterExpression(
): string {
const baseFilterParts = [
`kind_string = 'Client'`,
`(http.url EXISTS OR url.full EXISTS)`,
`(net.peer.name = '${domainName}' OR server.address = '${domainName}')`,
`${SPAN_ATTRIBUTES.HTTP_URL} EXISTS`,
`${SPAN_ATTRIBUTES.SERVER_NAME} = '${domainName}'`,
`has_error = true`,
];
if (showStatusCodeErrors) {
@@ -910,12 +874,7 @@ export const getTopErrorsQueryPayload = (
filter: { expression: filterExpression },
groupBy: [
{
name: 'http.url',
fieldDataType: 'string',
fieldContext: 'attribute',
},
{
name: 'url.full',
name: SPAN_ATTRIBUTES.HTTP_URL,
fieldDataType: 'string',
fieldContext: 'attribute',
},
@@ -1134,11 +1093,11 @@ export const formatEndPointsDataForTable = (
if (!isGroupedByAttribute) {
formattedData = data?.map((endpoint) => {
const { port } = extractPortAndEndpoint(
(endpoint.data[SPAN_ATTRIBUTES.URL_PATH] as string) || '',
(endpoint.data[SPAN_ATTRIBUTES.HTTP_URL] as string) || '',
);
return {
key: v4(),
endpointName: (endpoint.data[SPAN_ATTRIBUTES.URL_PATH] as string) || '-',
endpointName: (endpoint.data[SPAN_ATTRIBUTES.HTTP_URL] as string) || '-',
port,
callCount:
endpoint.data.A === 'n/a' || endpoint.data.A === undefined
@@ -1262,9 +1221,7 @@ export const formatTopErrorsDataForTable = (
return {
key: v4(),
endpointName: getDisplayValue(
rowObj[SPAN_ATTRIBUTES.URL_PATH] || rowObj['url.full'],
),
endpointName: getDisplayValue(rowObj[SPAN_ATTRIBUTES.HTTP_URL]),
statusCode: getDisplayValue(rowObj[SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE]),
statusMessage: getDisplayValue(rowObj.status_message),
count: getDisplayValue(rowObj.__result_0),
@@ -1281,10 +1238,10 @@ export const getTopErrorsCoRelationQueryFilters = (
{
id: 'ea16470b',
key: {
key: 'http.url',
key: SPAN_ATTRIBUTES.HTTP_URL,
dataType: DataTypes.String,
type: 'tag',
id: 'http.url--string--tag--false',
id: `${SPAN_ATTRIBUTES.HTTP_URL}--string--tag--false`,
},
op: '=',
value: endPointName,
@@ -1781,7 +1738,7 @@ export const getEndPointDetailsQueryPayload = (
filters || { items: [], op: 'AND' },
`${getDomainNameFilterExpression(
domainName,
)} AND ${clientKindExpression} AND (http.url EXISTS OR url.full EXISTS)`,
)} AND ${clientKindExpression} AND ${SPAN_ATTRIBUTES.HTTP_URL} EXISTS`,
),
},
expression: 'A',
@@ -1793,12 +1750,7 @@ export const getEndPointDetailsQueryPayload = (
orderBy: [],
groupBy: [
{
key: SPAN_ATTRIBUTES.URL_PATH,
dataType: DataTypes.String,
type: 'attribute',
},
{
key: 'url.full',
key: SPAN_ATTRIBUTES.HTTP_URL,
dataType: DataTypes.String,
type: 'attribute',
},
@@ -2225,7 +2177,7 @@ export const getEndPointZeroStateQueryPayload = (
orderBy: [],
groupBy: [
{
key: SPAN_ATTRIBUTES.URL_PATH,
key: SPAN_ATTRIBUTES.HTTP_URL,
dataType: DataTypes.String,
type: 'tag',
},
@@ -2419,8 +2371,7 @@ export const statusCodeWidgetInfo = [
interface EndPointDropDownResponseRow {
data: {
[SPAN_ATTRIBUTES.URL_PATH]: string;
'url.full': string;
[SPAN_ATTRIBUTES.HTTP_URL]: string;
A: number;
};
}
@@ -2439,8 +2390,8 @@ export const getFormattedEndPointDropDownData = (
}
return data.map((row) => ({
key: v4(),
label: row.data[SPAN_ATTRIBUTES.URL_PATH] || row.data['url.full'] || '-',
value: row.data[SPAN_ATTRIBUTES.URL_PATH] || row.data['url.full'] || '-',
label: row.data[SPAN_ATTRIBUTES.HTTP_URL] || '-',
value: row.data[SPAN_ATTRIBUTES.HTTP_URL] || '-',
}));
};
@@ -2769,7 +2720,6 @@ export const groupStatusCodes = (
export const getStatusCodeBarChartWidgetData = (
domainName: string,
endPointName: string,
filters: IBuilderQuery['filters'],
): Widgets => ({
query: {
@@ -2798,20 +2748,6 @@ export const getStatusCodeBarChartWidgetData = (
op: '=',
value: domainName,
},
...(endPointName
? [
{
id: '8b1be6f0',
key: {
dataType: DataTypes.String,
key: SPAN_ATTRIBUTES.URL_PATH,
type: 'tag',
},
op: '=',
value: endPointName,
},
]
: []),
...(filters?.items || []),
],
op: 'AND',
@@ -2933,7 +2869,7 @@ export const getAllEndpointsWidgetData = (
filters,
`${getDomainNameFilterExpression(
domainName,
)} AND ${clientKindExpression} AND (http.url EXISTS OR url.full EXISTS)`,
)} AND ${clientKindExpression} AND http_url EXISTS`,
),
},
functions: [],
@@ -2965,7 +2901,7 @@ export const getAllEndpointsWidgetData = (
filters,
`${getDomainNameFilterExpression(
domainName,
)} AND ${clientKindExpression} AND (http.url EXISTS OR url.full EXISTS)`,
)} AND ${clientKindExpression} AND http_url EXISTS`,
),
},
functions: [],
@@ -2997,7 +2933,7 @@ export const getAllEndpointsWidgetData = (
filters,
`${getDomainNameFilterExpression(
domainName,
)} AND ${clientKindExpression} AND (http.url EXISTS OR url.full EXISTS)`,
)} AND ${clientKindExpression} AND http_url EXISTS`,
),
},
functions: [],
@@ -3029,7 +2965,7 @@ export const getAllEndpointsWidgetData = (
filters,
`${getDomainNameFilterExpression(
domainName,
)} AND ${clientKindExpression} AND has_error = true AND (http.url EXISTS OR url.full EXISTS)`,
)} AND ${clientKindExpression} AND has_error = true AND http_url EXISTS`,
),
},
functions: [],
@@ -3060,24 +2996,12 @@ export const getAllEndpointsWidgetData = (
);
widget.renderColumnCell = {
[SPAN_ATTRIBUTES.URL_PATH]: (
url: string | number,
record?: RowData,
): ReactNode => {
// First try to use the url from the column value
let urlValue = url;
// If url is empty/null and we have the record, fallback to url.full
if (isEmptyFilterValue(url) && record) {
const { 'url.full': urlFull } = record;
urlValue = urlFull;
}
if (!urlValue || urlValue === 'n/a') {
[SPAN_ATTRIBUTES.HTTP_URL]: (url: string | number): ReactNode => {
if (isEmptyFilterValue(url) || !url || url === 'n/a') {
return <span>-</span>;
}
const { endpoint } = extractPortAndEndpoint(String(urlValue));
const { endpoint } = extractPortAndEndpoint(String(url));
return <span>{getDisplayValue(endpoint)}</span>;
},
A: (numOfCalls: any): ReactNode => (
@@ -3132,8 +3056,8 @@ export const getAllEndpointsWidgetData = (
};
widget.customColTitles = {
[SPAN_ATTRIBUTES.URL_PATH]: 'Endpoint',
'net.peer.port': 'Port',
[SPAN_ATTRIBUTES.HTTP_URL]: 'Endpoint',
[SPAN_ATTRIBUTES.SERVER_PORT]: 'Port',
};
widget.title = (
@@ -3158,12 +3082,10 @@ export const getAllEndpointsWidgetData = (
</div>
);
widget.hiddenColumns = ['url.full'];
return widget;
};
const keysToRemove = ['http.url', 'url.full', 'A', 'B', 'C', 'F1'];
const keysToRemove = [SPAN_ATTRIBUTES.HTTP_URL, 'A', 'B', 'C', 'F1'];
export const getGroupByFiltersFromGroupByValues = (
rowData: any,
@@ -3221,7 +3143,7 @@ export const getRateOverTimeWidgetData = (
filter: {
expression: convertFiltersWithUrlHandling(
filters || { items: [], op: 'AND' },
`(net.peer.name = '${domainName}' OR server.address = '${domainName}')`,
`http_host = '${domainName}'`,
),
},
functions: [],
@@ -3272,7 +3194,7 @@ export const getLatencyOverTimeWidgetData = (
filter: {
expression: convertFiltersWithUrlHandling(
filters || { items: [], op: 'AND' },
`(net.peer.name = '${domainName}' OR server.address = '${domainName}')`,
`http_host = '${domainName}'`,
),
},
functions: [],

View File

@@ -1,7 +1,6 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { useEffect, useMemo, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import {
@@ -15,14 +14,16 @@ import {
Tag,
Typography,
} from 'antd';
import updateSubDomainAPI from 'api/customDomain/updateSubDomain';
import {
RenderErrorResponseDTO,
ZeustypesHostDTO,
} from 'api/generated/services/sigNoz.schemas';
import { useGetHosts, usePutHost } from 'api/generated/services/zeus';
import { AxiosError } from 'axios';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { useGetDeploymentsData } from 'hooks/CustomDomain/useGetDeploymentsData';
import { useNotifications } from 'hooks/useNotifications';
import { InfoIcon, Link2, Pencil } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { HostsProps } from 'types/api/customDomain/types';
import './CustomDomainSettings.styles.scss';
@@ -35,7 +36,7 @@ export default function CustomDomainSettings(): JSX.Element {
const { notifications } = useNotifications();
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isPollingEnabled, setIsPollingEnabled] = useState(false);
const [hosts, setHosts] = useState<HostsProps[] | null>(null);
const [hosts, setHosts] = useState<ZeustypesHostDTO[] | null>(null);
const [updateDomainError, setUpdateDomainError] = useState<AxiosError | null>(
null,
@@ -57,36 +58,37 @@ export default function CustomDomainSettings(): JSX.Element {
};
const {
data: deploymentsData,
isLoading: isLoadingDeploymentsData,
isFetching: isFetchingDeploymentsData,
refetch: refetchDeploymentsData,
} = useGetDeploymentsData(true);
data: hostsData,
isLoading: isLoadingHosts,
isFetching: isFetchingHosts,
refetch: refetchHosts,
} = useGetHosts();
const {
mutate: updateSubDomain,
isLoading: isLoadingUpdateCustomDomain,
} = useMutation(updateSubDomainAPI, {
onSuccess: () => {
setIsPollingEnabled(true);
refetchDeploymentsData();
setIsEditModalOpen(false);
},
onError: (error: AxiosError) => {
setUpdateDomainError(error);
setIsPollingEnabled(false);
},
});
} = usePutHost<AxiosError<RenderErrorResponseDTO>>();
const stripProtocol = (url: string): string => {
return url?.split('://')[1] ?? url;
};
const dnsSuffix = useMemo(() => {
const defaultHost = hosts?.find((h) => h.is_default);
return defaultHost?.url && defaultHost?.name
? defaultHost.url.split(`${defaultHost.name}.`)[1] || ''
: '';
}, [hosts]);
useEffect(() => {
if (isFetchingDeploymentsData) {
if (isFetchingHosts) {
return;
}
if (deploymentsData?.data?.status === 'success') {
setHosts(deploymentsData.data.data.hosts);
if (hostsData?.data?.status === 'success') {
setHosts(hostsData?.data?.data?.hosts ?? null);
const activeCustomDomain = deploymentsData.data.data.hosts.find(
const activeCustomDomain = hostsData?.data?.data?.hosts?.find(
(host) => !host.is_default,
);
@@ -97,32 +99,36 @@ export default function CustomDomainSettings(): JSX.Element {
}
}
if (deploymentsData?.data?.data?.state !== 'HEALTHY' && isPollingEnabled) {
if (hostsData?.data?.data?.state !== 'HEALTHY' && isPollingEnabled) {
setTimeout(() => {
refetchDeploymentsData();
refetchHosts();
}, 3000);
}
if (deploymentsData?.data?.data.state === 'HEALTHY') {
if (hostsData?.data?.data?.state === 'HEALTHY') {
setIsPollingEnabled(false);
}
}, [
deploymentsData,
refetchDeploymentsData,
isPollingEnabled,
isFetchingDeploymentsData,
]);
}, [hostsData, refetchHosts, isPollingEnabled, isFetchingHosts]);
const onUpdateCustomDomainSettings = (): void => {
editForm
.validateFields()
.then((values) => {
if (values.subdomain) {
updateSubDomain({
data: {
name: values.subdomain,
updateSubDomain(
{ data: { name: values.subdomain } },
{
onSuccess: () => {
setIsPollingEnabled(true);
refetchHosts();
setIsEditModalOpen(false);
},
onError: (error: AxiosError<RenderErrorResponseDTO>) => {
setUpdateDomainError(error as AxiosError);
setIsPollingEnabled(false);
},
},
});
);
setCustomDomainDetails({
subdomain: values.subdomain,
@@ -134,10 +140,8 @@ export default function CustomDomainSettings(): JSX.Element {
});
};
const onCopyUrlHandler = (host: string): void => {
const url = `${host}.${deploymentsData?.data.data.cluster.region.dns}`;
setCopyUrl(url);
const onCopyUrlHandler = (url: string): void => {
setCopyUrl(stripProtocol(url));
notifications.success({
message: 'Copied to clipboard',
});
@@ -157,7 +161,7 @@ export default function CustomDomainSettings(): JSX.Element {
</div>
<div className="custom-domain-settings-content">
{!isLoadingDeploymentsData && (
{!isLoadingHosts && (
<Card className="custom-domain-settings-card">
<div className="custom-domain-settings-content-header">
Team {org?.[0]?.displayName} Information
@@ -169,10 +173,9 @@ export default function CustomDomainSettings(): JSX.Element {
<div
className="custom-domain-url"
key={host.name}
onClick={(): void => onCopyUrlHandler(host.name)}
onClick={(): void => onCopyUrlHandler(host.url || '')}
>
<Link2 size={12} /> {host.name}.
{deploymentsData?.data.data.cluster.region.dns}
<Link2 size={12} /> {stripProtocol(host.url || '')}
{host.is_default && <Tag color={Color.BG_ROBIN_500}>Default</Tag>}
</div>
))}
@@ -181,11 +184,7 @@ export default function CustomDomainSettings(): JSX.Element {
<div className="custom-domain-url-edit-btn">
<Button
className="periscope-btn"
disabled={
isLoadingDeploymentsData ||
isFetchingDeploymentsData ||
isPollingEnabled
}
disabled={isLoadingHosts || isFetchingHosts || isPollingEnabled}
type="default"
icon={<Pencil size={10} />}
onClick={(): void => setIsEditModalOpen(true)}
@@ -198,7 +197,7 @@ export default function CustomDomainSettings(): JSX.Element {
{isPollingEnabled && (
<Alert
className="custom-domain-update-status"
message={`Updating your URL to ⎯ ${customDomainDetails?.subdomain}.${deploymentsData?.data.data.cluster.region.dns}. This may take a few mins.`}
message={`Updating your URL to ⎯ ${customDomainDetails?.subdomain}.${dnsSuffix}. This may take a few mins.`}
type="info"
icon={<InfoIcon size={12} />}
/>
@@ -206,7 +205,7 @@ export default function CustomDomainSettings(): JSX.Element {
</Card>
)}
{isLoadingDeploymentsData && (
{isLoadingHosts && (
<Card className="custom-domain-settings-card">
<Skeleton
className="custom-domain-settings-skeleton"
@@ -255,7 +254,7 @@ export default function CustomDomainSettings(): JSX.Element {
addonBefore={updateDomainError && <InfoIcon size={12} color="red" />}
placeholder="Enter Domain"
onChange={(): void => setUpdateDomainError(null)}
addonAfter={deploymentsData?.data.data.cluster.region.dns}
addonAfter={dnsSuffix}
autoFocus
/>
</Form.Item>
@@ -267,7 +266,8 @@ export default function CustomDomainSettings(): JSX.Element {
{updateDomainError.status === 409 ? (
<Alert
message={
(updateDomainError?.response?.data as { error?: string })?.error ||
(updateDomainError?.response?.data as RenderErrorResponseDTO)?.error
?.message ||
'Youve already updated the custom domain once today. To make further changes, please contact our support team for assistance.'
}
type="warning"
@@ -275,7 +275,10 @@ export default function CustomDomainSettings(): JSX.Element {
/>
) : (
<Typography.Text type="danger">
{(updateDomainError.response?.data as { error: string })?.error}
{
(updateDomainError?.response?.data as RenderErrorResponseDTO)?.error
?.message
}
</Typography.Text>
)}
</div>

View File

@@ -0,0 +1,128 @@
import { GetHosts200 } from 'api/generated/services/sigNoz.schemas';
import { rest, server } from 'mocks-server/server';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import CustomDomainSettings from '../CustomDomainSettings';
const ZEUS_HOSTS_ENDPOINT = '*/api/v2/zeus/hosts';
const mockHostsResponse: GetHosts200 = {
status: 'success',
data: {
name: 'accepted-starfish',
state: 'HEALTHY',
tier: 'PREMIUM',
hosts: [
{
name: 'accepted-starfish',
is_default: true,
url: 'https://accepted-starfish.test.cloud',
},
{
name: 'custom-host',
is_default: false,
url: 'https://custom-host.test.cloud',
},
],
},
};
describe('CustomDomainSettings', () => {
afterEach(() => server.resetHandlers());
it('renders host URLs with protocol stripped and marks the default host', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
render(<CustomDomainSettings />);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
await screen.findByText(/custom-host\.test\.cloud/i);
expect(screen.getByText('Default')).toBeInTheDocument();
});
it('opens edit modal with DNS suffix derived from the default host', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /customize team[']s url/i }),
);
expect(
screen.getByRole('dialog', { name: /customize your team[']s url/i }),
).toBeInTheDocument();
// DNS suffix is the part of the default host URL after the name prefix
expect(screen.getByText('test.cloud')).toBeInTheDocument();
});
it('submits PUT to /zeus/hosts with the entered subdomain as the payload', async () => {
let capturedBody: Record<string, unknown> = {};
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
rest.put(ZEUS_HOSTS_ENDPOINT, async (req, res, ctx) => {
capturedBody = await req.json<Record<string, unknown>>();
return res(ctx.status(200), ctx.json({}));
}),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /customize team[']s url/i }),
);
const input = screen.getByPlaceholderText(/enter domain/i);
await user.clear(input);
await user.type(input, 'myteam');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
await waitFor(() => {
expect(capturedBody).toEqual({ name: 'myteam' });
});
});
it('shows contact support option when domain update returns 409', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
rest.put(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(
ctx.status(409),
ctx.json({ error: { message: 'Already updated today' } }),
),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /customize team[']s url/i }),
);
await user.type(screen.getByPlaceholderText(/enter domain/i), 'myteam');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
expect(
await screen.findByRole('button', { name: /contact support/i }),
).toBeInTheDocument();
});
});

View File

@@ -1,3 +1,22 @@
.chart-manager-series-label {
width: 100%;
min-width: 0;
max-width: 100%;
cursor: pointer;
border: none;
background-color: transparent;
color: inherit;
text-align: left;
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:disabled {
cursor: not-allowed;
}
}
.chart-manager-container {
width: 100%;
max-height: calc(40% - 40px);

View File

@@ -1,24 +1,28 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Button, Input } from 'antd';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import { ResizeTable } from 'components/ResizeTable';
import { getGraphManagerTableColumns } from 'container/GridCardLayout/GridCard/FullView/TableRender/GraphManagerColumns';
import { ExtendedChartDataset } from 'container/GridCardLayout/GridCard/FullView/types';
import { getDefaultTableDataSet } from 'container/GridCardLayout/GridCard/FullView/utils';
import { useNotifications } from 'hooks/useNotifications';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { usePlotContext } from 'lib/uPlotV2/context/PlotContext';
import useLegendsSync from 'lib/uPlotV2/hooks/useLegendsSync';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { getChartManagerColumns } from './getChartMangerColumns';
import { ExtendedChartDataset, getDefaultTableDataSet } from './utils';
import './ChartManager.styles.scss';
interface ChartManagerProps {
config: UPlotConfigBuilder;
alignedData: uPlot.AlignedData;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;
onCancel?: () => void;
}
const X_AXIS_INDEX = 0;
/**
* ChartManager provides a tabular view to manage the visibility of
* individual series on a uPlot chart.
@@ -28,16 +32,12 @@ interface ChartManagerProps {
* - filter series by label
* - toggle individual series on/off
* - persist the visibility configuration to local storage.
*
* @param config - `UPlotConfigBuilder` instance used to derive chart options.
* @param alignedData - uPlot aligned data used to build the initial table dataset.
* @param yAxisUnit - Optional unit label for Y-axis values shown in the table.
* @param onCancel - Optional callback invoked when the user cancels the dialog.
*/
export default function ChartManager({
config,
alignedData,
yAxisUnit,
decimalPrecision = PrecisionOptionsEnum.TWO,
onCancel,
}: ChartManagerProps): JSX.Element {
const { notifications } = useNotifications();
@@ -53,8 +53,13 @@ export default function ChartManager({
const { isDashboardLocked } = useDashboard();
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(() =>
getDefaultTableDataSet(config.getConfig() as uPlot.Options, alignedData),
getDefaultTableDataSet(
config.getConfig() as uPlot.Options,
alignedData,
decimalPrecision,
),
);
const [filterValue, setFilterValue] = useState('');
const graphVisibilityState = useMemo(
() =>
@@ -67,46 +72,62 @@ export default function ChartManager({
useEffect(() => {
setTableDataSet(
getDefaultTableDataSet(config.getConfig() as uPlot.Options, alignedData),
);
}, [alignedData, config]);
const filterHandler = useCallback(
(event: React.ChangeEvent<HTMLInputElement>): void => {
const value = event.target.value.toString().toLowerCase();
const updatedDataSet = tableDataSet.map((item) => {
if (item.label?.toLocaleLowerCase().includes(value)) {
return { ...item, show: true };
}
return { ...item, show: false };
});
setTableDataSet(updatedDataSet);
},
[tableDataSet],
);
const dataSource = useMemo(
() =>
tableDataSet.filter(
(item, index) => index !== 0 && item.show, // skipping the first item as it is the x-axis
getDefaultTableDataSet(
config.getConfig() as uPlot.Options,
alignedData,
decimalPrecision,
),
[tableDataSet],
);
setFilterValue('');
}, [alignedData, config, decimalPrecision]);
const handleFilterChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>): void => {
setFilterValue(e.target.value.toLowerCase());
},
[],
);
const handleToggleSeriesOnOff = useCallback(
(index: number): void => {
onToggleSeriesOnOff(index);
},
[onToggleSeriesOnOff],
);
const dataSource = useMemo(() => {
const filter = filterValue.trim();
return tableDataSet.filter((item, index) => {
if (index === X_AXIS_INDEX) {
return false;
}
if (!filter) {
return true;
}
return item.label?.toLowerCase().includes(filter) ?? false;
});
}, [tableDataSet, filterValue]);
const columns = useMemo(
() =>
getGraphManagerTableColumns({
getChartManagerColumns({
tableDataSet,
checkBoxOnChangeHandler: (_e, index) => {
onToggleSeriesOnOff(index);
},
graphVisibilityState,
labelClickedHandler: onToggleSeriesVisibility,
onToggleSeriesOnOff: handleToggleSeriesOnOff,
onToggleSeriesVisibility,
yAxisUnit,
isGraphDisabled: isDashboardLocked,
decimalPrecision,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[tableDataSet, graphVisibilityState, yAxisUnit, isDashboardLocked],
[
tableDataSet,
graphVisibilityState,
handleToggleSeriesOnOff,
onToggleSeriesVisibility,
yAxisUnit,
isDashboardLocked,
decimalPrecision,
],
);
const handleSave = useCallback((): void => {
@@ -114,15 +135,18 @@ export default function ChartManager({
notifications.success({
message: 'The updated graphs & legends are saved',
});
if (onCancel) {
onCancel();
}
onCancel?.();
}, [syncSeriesVisibilityToLocalStorage, notifications, onCancel]);
return (
<div className="chart-manager-container">
<div className="chart-manager-header">
<Input onChange={filterHandler} placeholder="Filter Series" />
<Input
placeholder="Filter Series"
value={filterValue}
onChange={handleFilterChange}
data-testid="filter-input"
/>
<div className="chart-manager-actions-container">
<Button type="default" onClick={onCancel}>
Cancel
@@ -136,10 +160,10 @@ export default function ChartManager({
<ResizeTable
columns={columns}
dataSource={dataSource}
virtual
rowKey="index"
scroll={{ y: 200 }}
pagination={false}
virtual
/>
</div>
</div>

View File

@@ -0,0 +1,31 @@
import { Tooltip } from 'antd';
import './ChartManager.styles.scss';
interface SeriesLabelProps {
label: string;
labelIndex: number;
onClick: (idx: number) => void;
disabled?: boolean;
}
export function SeriesLabel({
label,
labelIndex,
onClick,
disabled,
}: SeriesLabelProps): JSX.Element {
return (
<Tooltip placement="topLeft" title={label}>
<button
className="chart-manager-series-label"
disabled={disabled}
type="button"
data-testid={`series-label-button-${labelIndex}`}
onClick={(): void => onClick(labelIndex)}
>
{label}
</button>
</Tooltip>
);
}

View File

@@ -0,0 +1,172 @@
import userEvent from '@testing-library/user-event';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { render, screen } from 'tests/test-utils';
import ChartManager from '../ChartManager';
const mockSyncSeriesVisibilityToLocalStorage = jest.fn();
const mockNotificationsSuccess = jest.fn();
jest.mock('lib/uPlotV2/context/PlotContext', () => ({
usePlotContext: (): {
onToggleSeriesOnOff: jest.Mock;
onToggleSeriesVisibility: jest.Mock;
syncSeriesVisibilityToLocalStorage: jest.Mock;
} => ({
onToggleSeriesOnOff: jest.fn(),
onToggleSeriesVisibility: jest.fn(),
syncSeriesVisibilityToLocalStorage: mockSyncSeriesVisibilityToLocalStorage,
}),
}));
jest.mock('lib/uPlotV2/hooks/useLegendsSync', () => ({
__esModule: true,
default: (): {
legendItemsMap: { [key: number]: { show: boolean; label: string } };
} => ({
legendItemsMap: {
0: { show: true, label: 'Time' },
1: { show: true, label: 'Series 1' },
2: { show: true, label: 'Series 2' },
},
}),
}));
jest.mock('providers/Dashboard/Dashboard', () => ({
useDashboard: (): { isDashboardLocked: boolean } => ({
isDashboardLocked: false,
}),
}));
jest.mock('hooks/useNotifications', () => ({
useNotifications: (): { notifications: { success: jest.Mock } } => ({
notifications: {
success: mockNotificationsSuccess,
},
}),
}));
jest.mock('components/ResizeTable', () => {
const MockTable = ({
dataSource,
columns,
}: {
dataSource: { index: number; label?: string }[];
columns: { key: string; title: string }[];
}): JSX.Element => (
<div data-testid="resize-table">
{columns.map((col) => (
<span key={col.key}>{col.title}</span>
))}
{dataSource.map((row) => (
<div key={row.index} data-testid={`row-${row.index}`}>
{row.label}
</div>
))}
</div>
);
return { ResizeTable: MockTable };
});
const createMockConfig = (): { getConfig: () => uPlot.Options } => ({
getConfig: (): uPlot.Options => ({
width: 100,
height: 100,
series: [
{ label: 'Time', value: 'time' },
{ label: 'Series 1', scale: 'y' },
{ label: 'Series 2', scale: 'y' },
],
}),
});
const createAlignedData = (): uPlot.AlignedData => [
[1000, 2000, 3000],
[10, 20, 30],
[1, 2, 3],
];
describe('ChartManager', () => {
const mockOnCancel = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('renders filter input and action buttons', () => {
render(
<ChartManager
config={createMockConfig() as any}
alignedData={createAlignedData()}
onCancel={mockOnCancel}
/>,
);
expect(screen.getByPlaceholderText('Filter Series')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Cancel/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Save/ })).toBeInTheDocument();
});
it('renders ResizeTable with data', () => {
render(
<ChartManager
config={createMockConfig() as UPlotConfigBuilder}
alignedData={createAlignedData()}
/>,
);
expect(screen.getByTestId('resize-table')).toBeInTheDocument();
});
it('calls onCancel when Cancel button is clicked', async () => {
render(
<ChartManager
config={createMockConfig() as UPlotConfigBuilder}
alignedData={createAlignedData()}
onCancel={mockOnCancel}
/>,
);
await userEvent.click(screen.getByRole('button', { name: /Cancel/ }));
expect(mockOnCancel).toHaveBeenCalledTimes(1);
});
it('filters table data when typing in filter input', async () => {
render(
<ChartManager
config={createMockConfig() as UPlotConfigBuilder}
alignedData={createAlignedData()}
/>,
);
// Before filter: both Series 1 and Series 2 rows are visible
expect(screen.getByTestId('row-1')).toBeInTheDocument();
expect(screen.getByTestId('row-2')).toBeInTheDocument();
const filterInput = screen.getByTestId('filter-input');
await userEvent.type(filterInput, 'Series 1');
// After filter: only Series 1 row is visible, Series 2 row is filtered out
expect(screen.getByTestId('row-1')).toBeInTheDocument();
expect(screen.queryByTestId('row-2')).not.toBeInTheDocument();
});
it('calls syncSeriesVisibilityToLocalStorage, notifications.success, and onCancel when Save is clicked', async () => {
render(
<ChartManager
config={createMockConfig() as UPlotConfigBuilder}
alignedData={createAlignedData()}
onCancel={mockOnCancel}
/>,
);
await userEvent.click(screen.getByRole('button', { name: /Save/ }));
expect(mockSyncSeriesVisibilityToLocalStorage).toHaveBeenCalledTimes(1);
expect(mockNotificationsSuccess).toHaveBeenCalledWith({
message: 'The updated graphs & legends are saved',
});
expect(mockOnCancel).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,39 @@
import userEvent from '@testing-library/user-event';
import { render, screen } from 'tests/test-utils';
import { SeriesLabel } from '../SeriesLabel';
describe('SeriesLabel', () => {
it('renders the label text', () => {
render(
<SeriesLabel label="Test Series Label" labelIndex={1} onClick={jest.fn()} />,
);
expect(screen.getByTestId('series-label-button-1')).toHaveTextContent(
'Test Series Label',
);
});
it('calls onClick with labelIndex when clicked', async () => {
const onClick = jest.fn();
render(<SeriesLabel label="Series A" labelIndex={2} onClick={onClick} />);
await userEvent.click(screen.getByTestId('series-label-button-2'));
expect(onClick).toHaveBeenCalledWith(2);
expect(onClick).toHaveBeenCalledTimes(1);
});
it('renders disabled button when disabled prop is true', () => {
render(
<SeriesLabel label="Disabled" labelIndex={0} onClick={jest.fn()} disabled />,
);
const button = screen.getByTestId('series-label-button-0');
expect(button).toBeDisabled();
});
it('has chart-manager-series-label class', () => {
render(<SeriesLabel label="Label" labelIndex={0} onClick={jest.fn()} />);
const button = screen.getByTestId('series-label-button-0');
expect(button).toHaveClass('chart-manager-series-label');
});
});

View File

@@ -0,0 +1,167 @@
import { render } from '@testing-library/react';
import { Y_AXIS_UNIT_NAMES } from 'components/YAxisUnitSelector/constants';
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
import { getChartManagerColumns } from '../getChartMangerColumns';
import { ExtendedChartDataset } from '../utils';
const createMockDataset = (
index: number,
overrides: Partial<ExtendedChartDataset> = {},
): ExtendedChartDataset =>
({
index,
label: `Series ${index}`,
show: true,
sum: 100,
avg: 50,
min: 10,
max: 90,
stroke: '#ff0000',
...overrides,
} as ExtendedChartDataset);
describe('getChartManagerColumns', () => {
const tableDataSet: ExtendedChartDataset[] = [
createMockDataset(0, { label: 'Time' }),
createMockDataset(1),
createMockDataset(2),
];
const graphVisibilityState = [true, true, false];
const onToggleSeriesOnOff = jest.fn();
const onToggleSeriesVisibility = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('returns columns with expected structure', () => {
const columns = getChartManagerColumns({
tableDataSet,
graphVisibilityState,
onToggleSeriesOnOff,
onToggleSeriesVisibility,
});
expect(columns).toHaveLength(6);
expect(columns[0].key).toBe('index');
expect(columns[1].key).toBe('label');
expect(columns[2].key).toBe('avg');
expect(columns[3].key).toBe('sum');
expect(columns[4].key).toBe('max');
expect(columns[5].key).toBe('min');
});
it('includes Label column with title', () => {
const columns = getChartManagerColumns({
tableDataSet,
graphVisibilityState,
onToggleSeriesOnOff,
onToggleSeriesVisibility,
});
const labelCol = columns.find((c) => c.key === 'label');
expect(labelCol!.title).toBe('Label');
});
it('formats column titles with yAxisUnit', () => {
const columns = getChartManagerColumns({
tableDataSet,
graphVisibilityState,
onToggleSeriesOnOff,
onToggleSeriesVisibility,
yAxisUnit: 'ms',
});
const avgCol = columns.find((c) => c.key === 'avg');
expect(avgCol!.title).toBe(
`Avg (in ${Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MILLISECONDS]})`,
);
});
it('numeric column render returns formatted string with yAxisUnit', () => {
const columns = getChartManagerColumns({
tableDataSet,
graphVisibilityState,
onToggleSeriesOnOff,
onToggleSeriesVisibility,
yAxisUnit: 'ms',
});
const avgCol = columns.find((c) => c.key === 'avg');
const renderFn = avgCol?.render as
| ((val: number, record: ExtendedChartDataset, index: number) => string)
| undefined;
expect(renderFn).toBeDefined();
const output = renderFn!(123.45, tableDataSet[1], 1);
expect(output).toBe('123.45 ms');
});
it('numeric column render formats zero when value is undefined', () => {
const columns = getChartManagerColumns({
tableDataSet,
graphVisibilityState,
onToggleSeriesOnOff,
onToggleSeriesVisibility,
yAxisUnit: 'none',
});
const sumCol = columns.find((c) => c.key === 'sum');
const renderFn = sumCol?.render as
| ((
val: number | undefined,
record: ExtendedChartDataset,
index: number,
) => string)
| undefined;
const output = renderFn!(undefined, tableDataSet[1], 1);
expect(output).toBe('0');
});
it('label column render displays label text and is clickable', () => {
const columns = getChartManagerColumns({
tableDataSet,
graphVisibilityState,
onToggleSeriesOnOff,
onToggleSeriesVisibility,
});
const labelCol = columns.find((c) => c.key === 'label');
const renderFn = labelCol!.render as
| ((
label: string,
record: ExtendedChartDataset,
index: number,
) => JSX.Element)
| undefined;
expect(renderFn).toBeDefined();
const renderResult = renderFn!('Series 1', tableDataSet[1], 1);
const { getByRole } = render(renderResult);
expect(getByRole('button', { name: 'Series 1' })).toBeInTheDocument();
});
it('index column render renders checkbox with correct checked state', () => {
const columns = getChartManagerColumns({
tableDataSet,
graphVisibilityState,
onToggleSeriesOnOff,
onToggleSeriesVisibility,
});
const indexCol = columns.find((c) => c.key === 'index');
const renderFn = indexCol!.render as
| ((
_val: unknown,
record: ExtendedChartDataset,
index: number,
) => JSX.Element)
| undefined;
expect(renderFn).toBeDefined();
const { container } = render(renderFn!(null, tableDataSet[1], 1));
const checkbox = container.querySelector('input[type="checkbox"]');
expect(checkbox).toBeInTheDocument();
expect(checkbox).toBeChecked(); // graphVisibilityState[1] is true
});
});

View File

@@ -0,0 +1,113 @@
import { PrecisionOptionsEnum } from 'components/Graph/types';
import {
formatTableValueWithUnit,
getDefaultTableDataSet,
getTableColumnTitle,
} from '../utils';
describe('ChartManager utils', () => {
describe('getDefaultTableDataSet', () => {
const createOptions = (seriesCount: number): uPlot.Options => ({
series: Array.from({ length: seriesCount }, (_, i) =>
i === 0
? { label: 'Time', value: 'time' }
: { label: `Series ${i}`, scale: 'y' },
),
width: 100,
height: 100,
});
it('returns one row per series with computed stats', () => {
const options = createOptions(3);
const data: uPlot.AlignedData = [
[1000, 2000, 3000],
[10, 20, 30],
[1, 2, 3],
];
const result = getDefaultTableDataSet(options, data);
expect(result).toHaveLength(3);
expect(result[0]).toMatchObject({
index: 0,
label: 'Time',
show: true,
});
expect(result[1]).toMatchObject({
index: 1,
label: 'Series 1',
show: true,
sum: 60,
avg: 20,
max: 30,
min: 10,
});
expect(result[2]).toMatchObject({
index: 2,
label: 'Series 2',
show: true,
sum: 6,
avg: 2,
max: 3,
min: 1,
});
});
it('handles empty data arrays', () => {
const options = createOptions(2);
const data: uPlot.AlignedData = [[], []];
const result = getDefaultTableDataSet(options, data);
expect(result[0]).toMatchObject({
sum: 0,
avg: 0,
max: 0,
min: 0,
});
});
it('respects decimalPrecision parameter', () => {
const options = createOptions(2);
const data: uPlot.AlignedData = [[1000], [123.454]];
const resultTwo = getDefaultTableDataSet(
options,
data,
PrecisionOptionsEnum.TWO,
);
expect(resultTwo[1].avg).toBe(123.45);
const resultZero = getDefaultTableDataSet(
options,
data,
PrecisionOptionsEnum.ZERO,
);
expect(resultZero[1].avg).toBe(123);
});
});
describe('formatTableValueWithUnit', () => {
it('formats value with unit', () => {
const result = formatTableValueWithUnit(1234.56, 'ms');
expect(result).toBe('1.23 s');
});
it('falls back to none format when yAxisUnit is undefined', () => {
const result = formatTableValueWithUnit(123.45);
expect(result).toBe('123.45');
});
});
describe('getTableColumnTitle', () => {
it('returns title only when yAxisUnit is undefined', () => {
expect(getTableColumnTitle('Avg')).toBe('Avg');
});
it('returns title with unit when yAxisUnit is provided', () => {
const result = getTableColumnTitle('Avg', 'ms');
expect(result).toBe('Avg (in Milliseconds (ms))');
});
});
});

View File

@@ -0,0 +1,94 @@
import { ColumnType } from 'antd/es/table';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import CustomCheckBox from 'container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox';
import { SeriesLabel } from './SeriesLabel';
import {
ExtendedChartDataset,
formatTableValueWithUnit,
getTableColumnTitle,
} from './utils';
export interface GetChartManagerColumnsParams {
tableDataSet: ExtendedChartDataset[];
graphVisibilityState: boolean[];
onToggleSeriesOnOff: (index: number) => void;
onToggleSeriesVisibility: (index: number) => void;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;
isGraphDisabled?: boolean;
}
export function getChartManagerColumns({
tableDataSet,
graphVisibilityState,
onToggleSeriesOnOff,
onToggleSeriesVisibility,
yAxisUnit,
decimalPrecision = PrecisionOptionsEnum.TWO,
isGraphDisabled,
}: GetChartManagerColumnsParams): ColumnType<ExtendedChartDataset>[] {
return [
{
title: '',
width: 50,
dataIndex: 'index',
key: 'index',
render: (_: unknown, record: ExtendedChartDataset): JSX.Element => (
<CustomCheckBox
data={tableDataSet}
graphVisibilityState={graphVisibilityState}
index={record.index}
disabled={isGraphDisabled}
checkBoxOnChangeHandler={(_e, idx): void => onToggleSeriesOnOff(idx)}
/>
),
},
{
title: 'Label',
width: 300,
dataIndex: 'label',
key: 'label',
render: (label: string, record: ExtendedChartDataset): JSX.Element => (
<SeriesLabel
label={label ?? ''}
labelIndex={record.index}
disabled={isGraphDisabled}
onClick={onToggleSeriesVisibility}
/>
),
},
{
title: getTableColumnTitle('Avg', yAxisUnit),
width: 90,
dataIndex: 'avg',
key: 'avg',
render: (val: number | undefined): string =>
formatTableValueWithUnit(val ?? 0, yAxisUnit, decimalPrecision),
},
{
title: getTableColumnTitle('Sum', yAxisUnit),
width: 90,
dataIndex: 'sum',
key: 'sum',
render: (val: number | undefined): string =>
formatTableValueWithUnit(val ?? 0, yAxisUnit, decimalPrecision),
},
{
title: getTableColumnTitle('Max', yAxisUnit),
width: 90,
dataIndex: 'max',
key: 'max',
render: (val: number | undefined): string =>
formatTableValueWithUnit(val ?? 0, yAxisUnit, decimalPrecision),
},
{
title: getTableColumnTitle('Min', yAxisUnit),
width: 90,
dataIndex: 'min',
key: 'min',
render: (val: number | undefined): string =>
formatTableValueWithUnit(val ?? 0, yAxisUnit, decimalPrecision),
},
];
}

View File

@@ -0,0 +1,93 @@
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { Y_AXIS_UNIT_NAMES } from 'components/YAxisUnitSelector/constants';
import uPlot from 'uplot';
/** Extended series with computed stats for table display */
export type ExtendedChartDataset = uPlot.Series & {
show: boolean;
sum: number;
avg: number;
min: number;
max: number;
index: number;
};
function roundToDecimalPrecision(
value: number,
decimalPrecision: PrecisionOption = PrecisionOptionsEnum.TWO,
): number {
if (
typeof value !== 'number' ||
Number.isNaN(value) ||
value === Infinity ||
value === -Infinity
) {
return 0;
}
if (decimalPrecision === PrecisionOptionsEnum.FULL) {
return value;
}
// regex to match the decimal precision for the given decimal precision
const regex = new RegExp(`^-?\\d*\\.?0*\\d{0,${decimalPrecision}}`);
const matched = value ? value.toFixed(decimalPrecision).match(regex) : null;
return matched ? parseFloat(matched[0]) : 0;
}
/** Build table dataset from uPlot options and aligned data */
export function getDefaultTableDataSet(
options: uPlot.Options,
data: uPlot.AlignedData,
decimalPrecision: PrecisionOption = PrecisionOptionsEnum.TWO,
): ExtendedChartDataset[] {
return options.series.map(
(series: uPlot.Series, index: number): ExtendedChartDataset => {
const arr = (data[index] as number[]) ?? [];
const sum = arr.reduce((a, b) => a + b, 0) || 0;
const count = arr.length || 1;
const hasValues = arr.length > 0;
return {
...series,
index,
show: true,
sum: roundToDecimalPrecision(sum, decimalPrecision),
avg: roundToDecimalPrecision(sum / count, decimalPrecision),
max: hasValues
? roundToDecimalPrecision(Math.max(...arr), decimalPrecision)
: 0,
min: hasValues
? roundToDecimalPrecision(Math.min(...arr), decimalPrecision)
: 0,
};
},
);
}
/** Format numeric value for table display using yAxisUnit */
export function formatTableValueWithUnit(
value: number,
yAxisUnit?: string,
decimalPrecision: PrecisionOption = PrecisionOptionsEnum.TWO,
): string {
return `${getYAxisFormattedValue(
String(value),
yAxisUnit ?? 'none',
decimalPrecision,
)}`;
}
/** Format column header with optional unit */
export function getTableColumnTitle(title: string, yAxisUnit?: string): string {
if (!yAxisUnit) {
return title;
}
const universalName =
Y_AXIS_UNIT_NAMES[yAxisUnit as keyof typeof Y_AXIS_UNIT_NAMES];
if (!universalName) {
return `${title} (in ${yAxisUnit})`;
}
return `${title} (in ${universalName})`;
}

View File

@@ -96,6 +96,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
config={config}
alignedData={chartData}
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
onCancel={onToggleModelHandler}
/>
);
@@ -105,6 +106,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
chartData,
widget.yAxisUnit,
onToggleModelHandler,
widget.decimalPrecision,
]);
const onPlotDestroy = useCallback(() => {

View File

@@ -95,6 +95,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
config={config}
alignedData={chartData}
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
onCancel={onToggleModelHandler}
/>
);
@@ -104,6 +105,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
chartData,
widget.yAxisUnit,
onToggleModelHandler,
widget.decimalPrecision,
]);
return (

View File

@@ -48,7 +48,9 @@ function ForgotPassword({
}
try {
ErrorResponseHandlerV2(mutationError as AxiosError<ErrorV2Resp>);
ErrorResponseHandlerV2(
(mutationError as unknown) as AxiosError<ErrorV2Resp>,
);
} catch (apiError) {
return apiError as APIError;
}

View File

@@ -49,7 +49,7 @@ function evaluateCondition(
* @param columnUnit - The current unit of the value.
* @returns A boolean indicating whether the value meets the threshold condition.
*/
function evaluateThresholdWithConvertedValue(
export function evaluateThresholdWithConvertedValue(
value: number,
thresholdValue: number,
thresholdOperator?: string,

View File

@@ -99,6 +99,7 @@ function GridValueComponent({
<ValueGraph
thresholds={thresholds || []}
rawValue={value}
yAxisUnit={yAxisUnit}
value={
yAxisUnit
? getYAxisFormattedValue(

View File

@@ -2,10 +2,10 @@
import { useEffect, useState } from 'react';
import { Button, Skeleton, Tag, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetHosts } from 'api/generated/services/zeus';
import ROUTES from 'constants/routes';
import { useGetDeploymentsData } from 'hooks/CustomDomain/useGetDeploymentsData';
import history from 'lib/history';
import { Globe, Link2 } from 'lucide-react';
import { Link2 } from 'lucide-react';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
@@ -26,36 +26,21 @@ function DataSourceInfo({
const isEnabled =
activeLicense && activeLicense.platform === LicensePlatform.CLOUD;
const {
data: deploymentsData,
isError: isErrorDeploymentsData,
} = useGetDeploymentsData(isEnabled || false);
const { data: hostsData, isError } = useGetHosts({
query: { enabled: isEnabled || false },
});
const [region, setRegion] = useState<string>('');
const [url, setUrl] = useState<string>('');
useEffect(() => {
if (deploymentsData) {
switch (deploymentsData?.data.data.cluster.region.name) {
case 'in':
setRegion('India');
break;
case 'us':
setRegion('United States');
break;
case 'eu':
setRegion('Europe');
break;
default:
setRegion(deploymentsData?.data.data.cluster.region.name);
break;
if (hostsData) {
const defaultHost = hostsData?.data?.data?.hosts?.find((h) => h.is_default);
if (defaultHost?.url) {
const url = defaultHost?.url?.split('://')[1] ?? '';
setUrl(url);
}
setUrl(
`${deploymentsData?.data.data.name}.${deploymentsData?.data.data.cluster.region.dns}`,
);
}
}, [deploymentsData]);
}, [hostsData]);
const renderNotSendingData = (): JSX.Element => (
<>
@@ -123,14 +108,8 @@ function DataSourceInfo({
</Button>
</div>
{!isErrorDeploymentsData && deploymentsData && (
{!isError && hostsData && (
<div className="workspace-details">
<div className="workspace-region">
<Globe size={10} />
<Typography>{region}</Typography>
</div>
<div className="workspace-url">
<Link2 size={12} />
@@ -156,17 +135,11 @@ function DataSourceInfo({
Hello there, Welcome to your SigNoz workspace
</Typography>
{!isErrorDeploymentsData && deploymentsData && (
{!isError && hostsData && (
<Card className="welcome-card">
<Card.Content>
<div className="workspace-ready-container">
<div className="workspace-details">
<div className="workspace-region">
<Globe size={10} />
<Typography>{region}</Typography>
</div>
<div className="workspace-url">
<Link2 size={12} />

View File

@@ -0,0 +1,69 @@
import { GetHosts200 } from 'api/generated/services/sigNoz.schemas';
import { rest, server } from 'mocks-server/server';
import { render, screen } from 'tests/test-utils';
import DataSourceInfo from '../DataSourceInfo';
const ZEUS_HOSTS_ENDPOINT = '*/api/v2/zeus/hosts';
const mockHostsResponse: GetHosts200 = {
status: 'success',
data: {
name: 'accepted-starfish',
state: 'HEALTHY',
tier: 'PREMIUM',
hosts: [
{
name: 'accepted-starfish',
is_default: true,
url: 'https://accepted-starfish.test.cloud',
},
{
name: 'custom-host',
is_default: false,
url: 'https://custom-host.test.cloud',
},
],
},
};
describe('DataSourceInfo', () => {
afterEach(() => server.resetHandlers());
it('renders the default workspace URL with protocol stripped', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
render(<DataSourceInfo dataSentToSigNoz={false} isLoading={false} />);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
});
it('does not render workspace URL when GET /zeus/hosts fails', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(500), ctx.json({})),
),
);
render(<DataSourceInfo dataSentToSigNoz={false} isLoading={false} />);
await screen.findByText(/Your workspace is ready/i);
expect(screen.queryByText(/signoz\.cloud/i)).not.toBeInTheDocument();
});
it('renders workspace URL in the data-received view when telemetry is flowing', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
render(<DataSourceInfo dataSentToSigNoz={true} isLoading={false} />);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
});
});

View File

@@ -178,7 +178,9 @@ export default function HostsListTable({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
tableLayout="fixed"
rowKey={(record): string => record.hostName}
rowKey={(record): string =>
(record as HostRowData & { key: string }).key ?? record.hostName
}
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),

View File

@@ -126,7 +126,8 @@
background: var(--bg-ink-500);
}
.ant-table-cell:has(.hostname-column-value) {
.ant-table-cell:has(.hostname-column-value),
.ant-table-cell:has(.hostname-cell-missing) {
background: var(--bg-ink-400);
}
@@ -139,6 +140,23 @@
letter-spacing: -0.07px;
}
.hostname-cell-missing {
display: flex;
align-items: center;
gap: 4px;
}
.hostname-cell-placeholder {
color: var(--Vanilla-400, #c0c1c3);
}
.hostname-cell-warning-icon {
display: inline-flex;
align-items: center;
margin-left: 4px;
cursor: help;
}
.status-cell {
.active-tag {
color: var(--bg-forest-500);
@@ -357,7 +375,8 @@
color: var(--bg-ink-500);
}
.ant-table-cell:has(.hostname-column-value) {
.ant-table-cell:has(.hostname-column-value),
.ant-table-cell:has(.hostname-cell-missing) {
background: var(--bg-vanilla-100);
}
@@ -365,6 +384,10 @@
color: var(--bg-ink-300);
}
.hostname-cell-placeholder {
color: var(--text-ink-300);
}
.ant-table-tbody > tr:hover > td {
background: rgba(0, 0, 0, 0.04);
}

View File

@@ -1,13 +1,24 @@
import { render } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { HostData, TimeSeries } from 'api/infraMonitoring/getHostLists';
import { formatDataForTable, GetHostsQuickFiltersConfig } from '../utils';
import {
formatDataForTable,
GetHostsQuickFiltersConfig,
HostnameCell,
} from '../utils';
const PROGRESS_BAR_CLASS = '.progress-bar';
const emptyTimeSeries: TimeSeries = {
labels: {},
labelsArray: [],
values: [],
};
describe('InfraMonitoringHosts utils', () => {
describe('formatDataForTable', () => {
it('should format host data correctly', () => {
const mockData = [
const mockData: HostData[] = [
{
hostName: 'test-host',
active: true,
@@ -16,8 +27,12 @@ describe('InfraMonitoringHosts utils', () => {
wait: 0.05,
load15: 2.5,
os: 'linux',
cpuTimeSeries: emptyTimeSeries,
memoryTimeSeries: emptyTimeSeries,
waitTimeSeries: emptyTimeSeries,
load15TimeSeries: emptyTimeSeries,
},
] as any;
];
const result = formatDataForTable(mockData);
@@ -46,7 +61,7 @@ describe('InfraMonitoringHosts utils', () => {
});
it('should handle inactive hosts', () => {
const mockData = [
const mockData: HostData[] = [
{
hostName: 'test-host',
active: false,
@@ -55,12 +70,12 @@ describe('InfraMonitoringHosts utils', () => {
wait: 0.02,
load15: 1.2,
os: 'linux',
cpuTimeSeries: [],
memoryTimeSeries: [],
waitTimeSeries: [],
load15TimeSeries: [],
cpuTimeSeries: emptyTimeSeries,
memoryTimeSeries: emptyTimeSeries,
waitTimeSeries: emptyTimeSeries,
load15TimeSeries: emptyTimeSeries,
},
] as any;
];
const result = formatDataForTable(mockData);
@@ -68,6 +83,65 @@ describe('InfraMonitoringHosts utils', () => {
expect(inactiveTag.container.textContent).toBe('INACTIVE');
expect(inactiveTag.container.querySelector('.inactive')).toBeTruthy();
});
it('should set hostName to empty string when host has no hostname', () => {
const mockData: HostData[] = [
{
hostName: '',
active: true,
cpu: 0.5,
memory: 0.4,
wait: 0.01,
load15: 1.0,
os: 'linux',
cpuTimeSeries: emptyTimeSeries,
memoryTimeSeries: emptyTimeSeries,
waitTimeSeries: emptyTimeSeries,
load15TimeSeries: emptyTimeSeries,
},
];
const result = formatDataForTable(mockData);
expect(result[0].hostName).toBe('');
expect(result[0].key).toBe('-0');
});
});
describe('HostnameCell', () => {
it('should render hostname when present (case A: no icon)', () => {
const { container } = render(<HostnameCell hostName="gke-prod-1" />);
expect(container.querySelector('.hostname-column-value')).toBeTruthy();
expect(container.textContent).toBe('gke-prod-1');
expect(container.querySelector('.hostname-cell-missing')).toBeFalsy();
expect(container.querySelector('.hostname-cell-warning-icon')).toBeFalsy();
});
it('should render placeholder and icon when hostName is empty (case B)', () => {
const { container } = render(<HostnameCell hostName="" />);
expect(screen.getByText('-')).toBeTruthy();
expect(container.querySelector('.hostname-cell-missing')).toBeTruthy();
const iconWrapper = container.querySelector('.hostname-cell-warning-icon');
expect(iconWrapper).toBeTruthy();
expect(iconWrapper?.getAttribute('aria-label')).toBe(
'Missing host.name metadata',
);
expect(iconWrapper?.getAttribute('tabindex')).toBe('0');
// Tooltip with "Learn how to configure →" link is shown on hover/focus
});
it('should render placeholder and icon when hostName is whitespace only (case C)', () => {
const { container } = render(<HostnameCell hostName=" " />);
expect(screen.getByText('-')).toBeTruthy();
expect(container.querySelector('.hostname-cell-missing')).toBeTruthy();
expect(container.querySelector('.hostname-cell-warning-icon')).toBeTruthy();
});
it('should render placeholder and icon when hostName is undefined (case D)', () => {
const { container } = render(<HostnameCell hostName={undefined} />);
expect(screen.getByText('-')).toBeTruthy();
expect(container.querySelector('.hostname-cell-missing')).toBeTruthy();
expect(container.querySelector('.hostname-cell-warning-icon')).toBeTruthy();
});
});
describe('GetHostsQuickFiltersConfig', () => {

View File

@@ -1,7 +1,7 @@
import { Dispatch, SetStateAction } from 'react';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Progress, TabsProps, Tag, Tooltip } from 'antd';
import { Progress, TabsProps, Tag, Tooltip, Typography } from 'antd';
import { ColumnType } from 'antd/es/table';
import {
HostData,
@@ -14,6 +14,7 @@ import {
} from 'components/QuickFilters/types';
import TabLabel from 'components/TabLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { TriangleAlert } from 'lucide-react';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
@@ -24,6 +25,7 @@ import HostsList from './HostsList';
import './InfraMonitoring.styles.scss';
export interface HostRowData {
key?: string;
hostName: string;
cpu: React.ReactNode;
memory: React.ReactNode;
@@ -32,6 +34,59 @@ export interface HostRowData {
active: React.ReactNode;
}
const HOSTNAME_DOCS_URL =
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#host-name-is-blankempty';
export function HostnameCell({
hostName,
}: {
hostName?: string | null;
}): React.ReactElement {
const isEmpty = !hostName || !hostName.trim();
if (!isEmpty) {
return <div className="hostname-column-value">{hostName}</div>;
}
return (
<div className="hostname-cell-missing">
<Typography.Text type="secondary" className="hostname-cell-placeholder">
-
</Typography.Text>
<Tooltip
title={
<div>
Missing host.name metadata.
<br />
<a
href={HOSTNAME_DOCS_URL}
target="_blank"
rel="noopener noreferrer"
onClick={(e): void => e.stopPropagation()}
>
Learn how to configure
</a>
</div>
}
trigger={['hover', 'focus']}
>
<span
className="hostname-cell-warning-icon"
tabIndex={0}
role="img"
aria-label="Missing host.name metadata"
onClick={(e): void => e.stopPropagation()}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
e.stopPropagation();
}
}}
>
<TriangleAlert size={14} color={Color.BG_CHERRY_500} />
</span>
</Tooltip>
</div>
);
}
export interface HostsListTableProps {
isLoading: boolean;
isError: boolean;
@@ -75,8 +130,8 @@ export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
dataIndex: 'hostName',
key: 'hostName',
width: 250,
render: (value: string): React.ReactNode => (
<div className="hostname-column-value">{value}</div>
render: (value: string | undefined): React.ReactNode => (
<HostnameCell hostName={value ?? ''} />
),
},
{

View File

@@ -342,7 +342,7 @@ function MultiIngestionSettings(): JSX.Element {
useEffect(() => {
if (isError) {
showErrorNotification(notifications, error as AxiosError);
showErrorNotification(notifications, (error as unknown) as AxiosError);
}
}, [error, isError, notifications]);

View File

@@ -36,24 +36,6 @@ jest.mock('react-router-dom', () => {
};
});
// Mock deployments data hook to avoid unrelated network calls in this page
jest.mock(
'hooks/CustomDomain/useGetDeploymentsData',
(): Record<string, unknown> => ({
useGetDeploymentsData: (): {
data: undefined;
isLoading: boolean;
isFetching: boolean;
isError: boolean;
} => ({
data: undefined,
isLoading: false,
isFetching: false,
isError: false,
}),
}),
);
const TEST_CREATED_UPDATED = '2024-01-01T00:00:00Z';
const TEST_EXPIRES_AT = '2030-01-01T00:00:00Z';
const TEST_WORKSPACE_ID = 'w1';

View File

@@ -1,3 +1,7 @@
import { YAxisCategoryNames } from 'components/YAxisUnitSelector/constants';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import { getYAxisCategories } from 'components/YAxisUnitSelector/utils';
import { convertValue } from 'lib/getConvertedValue';
import { flattenDeep } from 'lodash-es';
import {
@@ -439,131 +443,6 @@ export const flattenedCategories = flattenDeep(
dataTypeCategories.map((category) => category.formats),
);
type ConversionFactors = {
[key: string]: {
[key: string]: number | null;
};
};
// Object containing conversion factors for various categories and formats
const conversionFactors: ConversionFactors = {
[CategoryNames.Time]: {
[TimeFormats.Hertz]: 1,
[TimeFormats.Nanoseconds]: 1e-9,
[TimeFormats.Microseconds]: 1e-6,
[TimeFormats.Milliseconds]: 1e-3,
[TimeFormats.Seconds]: 1,
[TimeFormats.Minutes]: 60,
[TimeFormats.Hours]: 3600,
[TimeFormats.Days]: 86400,
[TimeFormats.DurationMs]: 1e-3,
[TimeFormats.DurationS]: 1,
[TimeFormats.DurationHms]: null, // Requires special handling
[TimeFormats.DurationDhms]: null, // Requires special handling
[TimeFormats.Timeticks]: null, // Requires special handling
[TimeFormats.ClockMs]: 1e-3,
[TimeFormats.ClockS]: 1,
},
[CategoryNames.Throughput]: {
[ThroughputFormats.CountsPerSec]: 1,
[ThroughputFormats.OpsPerSec]: 1,
[ThroughputFormats.RequestsPerSec]: 1,
[ThroughputFormats.ReadsPerSec]: 1,
[ThroughputFormats.WritesPerSec]: 1,
[ThroughputFormats.IOOpsPerSec]: 1,
[ThroughputFormats.CountsPerMin]: 1 / 60,
[ThroughputFormats.OpsPerMin]: 1 / 60,
[ThroughputFormats.ReadsPerMin]: 1 / 60,
[ThroughputFormats.WritesPerMin]: 1 / 60,
},
[CategoryNames.Data]: {
[DataFormats.BytesIEC]: 1,
[DataFormats.BytesSI]: 1,
[DataFormats.BitsIEC]: 0.125,
[DataFormats.BitsSI]: 0.125,
[DataFormats.KibiBytes]: 1024,
[DataFormats.KiloBytes]: 1000,
[DataFormats.MebiBytes]: 1048576,
[DataFormats.MegaBytes]: 1000000,
[DataFormats.GibiBytes]: 1073741824,
[DataFormats.GigaBytes]: 1000000000,
[DataFormats.TebiBytes]: 1099511627776,
[DataFormats.TeraBytes]: 1000000000000,
[DataFormats.PebiBytes]: 1125899906842624,
[DataFormats.PetaBytes]: 1000000000000000,
},
[CategoryNames.DataRate]: {
[DataRateFormats.PacketsPerSec]: null, // Cannot convert directly to other data rates
[DataRateFormats.BytesPerSecIEC]: 1,
[DataRateFormats.BytesPerSecSI]: 1,
[DataRateFormats.BitsPerSecIEC]: 0.125,
[DataRateFormats.BitsPerSecSI]: 0.125,
[DataRateFormats.KibiBytesPerSec]: 1024,
[DataRateFormats.KibiBitsPerSec]: 128,
[DataRateFormats.KiloBytesPerSec]: 1000,
[DataRateFormats.KiloBitsPerSec]: 125,
[DataRateFormats.MebiBytesPerSec]: 1048576,
[DataRateFormats.MebiBitsPerSec]: 131072,
[DataRateFormats.MegaBytesPerSec]: 1000000,
[DataRateFormats.MegaBitsPerSec]: 125000,
[DataRateFormats.GibiBytesPerSec]: 1073741824,
[DataRateFormats.GibiBitsPerSec]: 134217728,
[DataRateFormats.GigaBytesPerSec]: 1000000000,
[DataRateFormats.GigaBitsPerSec]: 125000000,
[DataRateFormats.TebiBytesPerSec]: 1099511627776,
[DataRateFormats.TebiBitsPerSec]: 137438953472,
[DataRateFormats.TeraBytesPerSec]: 1000000000000,
[DataRateFormats.TeraBitsPerSec]: 125000000000,
[DataRateFormats.PebiBytesPerSec]: 1125899906842624,
[DataRateFormats.PebiBitsPerSec]: 140737488355328,
[DataRateFormats.PetaBytesPerSec]: 1000000000000000,
[DataRateFormats.PetaBitsPerSec]: 125000000000000,
},
[CategoryNames.Miscellaneous]: {
[MiscellaneousFormats.None]: null,
[MiscellaneousFormats.String]: null,
[MiscellaneousFormats.Short]: null,
[MiscellaneousFormats.Percent]: 1,
[MiscellaneousFormats.PercentUnit]: 100,
[MiscellaneousFormats.Humidity]: 1,
[MiscellaneousFormats.Decibel]: null,
[MiscellaneousFormats.Hexadecimal0x]: null,
[MiscellaneousFormats.Hexadecimal]: null,
[MiscellaneousFormats.ScientificNotation]: null,
[MiscellaneousFormats.LocaleFormat]: null,
[MiscellaneousFormats.Pixels]: null,
},
[CategoryNames.Boolean]: {
[BooleanFormats.TRUE_FALSE]: null, // Not convertible
[BooleanFormats.YES_NO]: null, // Not convertible
[BooleanFormats.ON_OFF]: null, // Not convertible
},
};
// Function to get the conversion factor between two units in a specific category
function getConversionFactor(
fromUnit: string,
toUnit: string,
category: CategoryNames,
): number | null {
// Retrieves the conversion factors for the specified category
const categoryFactors = conversionFactors[category];
if (!categoryFactors) {
return null; // Returns null if the category does not exist
}
const fromFactor = categoryFactors[fromUnit];
const toFactor = categoryFactors[toUnit];
if (
fromFactor === undefined ||
toFactor === undefined ||
fromFactor === null ||
toFactor === null
) {
return null; // Returns null if either unit does not exist or is not convertible
}
return fromFactor / toFactor; // Returns the conversion factor ratio
}
// Function to convert a value from one unit to another
export function convertUnit(
value: number,
@@ -573,14 +452,16 @@ export function convertUnit(
let fromUnit: string | undefined;
let toUnit: string | undefined;
const categories = getYAxisCategories(YAxisSource.DASHBOARDS);
// Finds the category that contains the specified units and extracts fromUnit and toUnit using array methods
const category = dataTypeCategories.find((category) =>
category.formats.some((format) => {
if (format.id === fromUnitId) {
fromUnit = format.id;
const category = categories.find((category) =>
category.units.some((unit) => {
if (unit.id === fromUnitId) {
fromUnit = unit.id;
}
if (format.id === toUnitId) {
toUnit = format.id;
if (unit.id === toUnitId) {
toUnit = unit.id;
}
return fromUnit && toUnit; // Break out early if both units are found
}),
@@ -590,24 +471,16 @@ export function convertUnit(
return null;
} // Return null if category or units are not found
// Gets the conversion factor for the specified units
const conversionFactor = getConversionFactor(
fromUnit,
toUnit,
category.name as any,
);
if (conversionFactor === null) {
return null;
} // Return null if conversion is not possible
return value * conversionFactor;
// Convert the value from the fromUnit to the toUnit
return convertValue(value, fromUnit, toUnit);
}
// Function to get the category name for a given unit ID
export const getCategoryName = (unitId: string): CategoryNames | null => {
export const getCategoryName = (unitId: string): YAxisCategoryNames | null => {
// Finds the category that contains the specified unit ID
const foundCategory = dataTypeCategories.find((category) =>
category.formats.some((format) => format.id === unitId),
const categories = getYAxisCategories(YAxisSource.DASHBOARDS);
const foundCategory = categories.find((category) =>
category.units.some((unit) => unit.id === unitId),
);
return foundCategory ? (foundCategory.name as CategoryNames) : null;
return foundCategory ? foundCategory.name : null;
};

View File

@@ -2,6 +2,9 @@ import { Layout } from 'react-grid-layout';
import { DefaultOptionType } from 'antd/es/select';
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
import { PrecisionOptionsEnum } from 'components/Graph/types';
import { YAxisCategoryNames } from 'components/YAxisUnitSelector/constants';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import { getYAxisCategories } from 'components/YAxisUnitSelector/utils';
import {
initialQueryBuilderFormValuesMap,
PANEL_TYPES,
@@ -21,11 +24,7 @@ import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import {
dataTypeCategories,
getCategoryName,
} from './RightContainer/dataFormatCategories';
import { CategoryNames } from './RightContainer/types';
import { getCategoryName } from './RightContainer/dataFormatCategories';
export const getIsQueryModified = (
currentQuery: Query,
@@ -606,14 +605,21 @@ export const PANEL_TYPE_TO_QUERY_TYPES: Record<PANEL_TYPES, EQueryType[]> = {
* the label and value for each format.
*/
export const getCategorySelectOptionByName = (
name?: CategoryNames | string,
): DefaultOptionType[] =>
dataTypeCategories
.find((category) => category.name === name)
?.formats.map((format) => ({
label: format.name,
value: format.id,
})) || [];
name?: YAxisCategoryNames,
): DefaultOptionType[] => {
const categories = getYAxisCategories(YAxisSource.DASHBOARDS);
if (!categories.length) {
return [];
}
return (
categories
.find((category) => category.name === name)
?.units.map((unit) => ({
label: unit.name,
value: unit.id,
})) || []
);
};
/**
* Generates unit options based on the provided column unit.

View File

@@ -26,7 +26,7 @@ jest.mock('lib/history', () => ({
// API Endpoints
const ORG_PREFERENCES_ENDPOINT = '*/api/v1/org/preferences/list';
const UPDATE_ORG_PREFERENCE_ENDPOINT = '*/api/v1/org/preferences/name/update';
const UPDATE_PROFILE_ENDPOINT = '*/api/gateway/v2/profiles/me';
const UPDATE_PROFILE_ENDPOINT = '*/api/v2/zeus/profiles';
const EDIT_ORG_ENDPOINT = '*/api/v2/orgs/me';
const INVITE_USERS_ENDPOINT = '*/api/v1/invite/bulk/create';
@@ -277,6 +277,46 @@ describe('OnboardingQuestionaire Component', () => {
).toBeInTheDocument();
});
it('fires PUT to /zeus/profiles and advances to step 4 on success', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
let profilePutCalled = false;
server.use(
rest.put(UPDATE_PROFILE_ENDPOINT, (_, res, ctx) => {
profilePutCalled = true;
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
}),
);
render(<OnboardingQuestionaire />);
// Navigate to step 3
await user.click(screen.getByLabelText(/datadog/i));
await user.click(screen.getByRole('radio', { name: /yes/i }));
await user.click(screen.getByLabelText(/just exploring/i));
await user.click(screen.getByRole('button', { name: /next/i }));
await user.type(
await screen.findByPlaceholderText(/e\.g\., googling/i),
'Found via Google',
);
await user.click(screen.getByLabelText(/lowering observability costs/i));
await user.click(screen.getByRole('button', { name: /next/i }));
// Click "I'll do this later" on step 3 — triggers PUT /zeus/profiles
await user.click(
await screen.findByRole('button', { name: /i'll do this later/i }),
);
await waitFor(() => {
expect(profilePutCalled).toBe(true);
// Step 3 content is gone — successfully advanced to step 4
expect(
screen.queryByText(/what does your scale approximately look like/i),
).not.toBeInTheDocument();
});
});
it('shows do later button', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<OnboardingQuestionaire />);

View File

@@ -1,8 +1,10 @@
import { useEffect, useState } from 'react';
import { useMutation, useQuery } from 'react-query';
import { toast } from '@signozhq/sonner';
import { NotificationInstance } from 'antd/es/notification/interface';
import logEvent from 'api/common/logEvent';
import updateProfileAPI from 'api/onboarding/updateProfile';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { usePutProfile } from 'api/generated/services/zeus';
import listOrgPreferences from 'api/v1/org/preferences/list';
import updateOrgPreferenceAPI from 'api/v1/org/preferences/name/update';
import { AxiosError } from 'axios';
@@ -121,20 +123,9 @@ function OnboardingQuestionaire(): JSX.Element {
optimiseSignozDetails.hostsPerDay === 0 &&
optimiseSignozDetails.services === 0;
const { mutate: updateProfile, isLoading: isUpdatingProfile } = useMutation(
updateProfileAPI,
{
onSuccess: () => {
setCurrentStep(4);
},
onError: (error) => {
showErrorNotification(notifications, error as AxiosError);
// Allow user to proceed even if API fails
setCurrentStep(4);
},
},
);
const { mutate: updateProfile, isLoading: isUpdatingProfile } = usePutProfile<
AxiosError<RenderErrorResponseDTO>
>();
const { mutate: updateOrgPreference } = useMutation(updateOrgPreferenceAPI, {
onSuccess: () => {
@@ -153,29 +144,44 @@ function OnboardingQuestionaire(): JSX.Element {
nextPageID: 4,
});
updateProfile({
uses_otel: orgDetails?.usesOtel as boolean,
has_existing_observability_tool: orgDetails?.usesObservability as boolean,
existing_observability_tool:
orgDetails?.observabilityTool === 'Others'
? (orgDetails?.otherTool as string)
: (orgDetails?.observabilityTool as string),
where_did_you_discover_signoz: signozDetails?.discoverSignoz as string,
timeline_for_migrating_to_signoz: orgDetails?.migrationTimeline as string,
reasons_for_interest_in_signoz: signozDetails?.interestInSignoz?.includes(
'Others',
)
? ([
...(signozDetails?.interestInSignoz?.filter(
(item) => item !== 'Others',
) || []),
signozDetails?.otherInterestInSignoz,
] as string[])
: (signozDetails?.interestInSignoz as string[]),
logs_scale_per_day_in_gb: optimiseSignozDetails?.logsPerDay as number,
number_of_hosts: optimiseSignozDetails?.hostsPerDay as number,
number_of_services: optimiseSignozDetails?.services as number,
});
updateProfile(
{
data: {
uses_otel: orgDetails?.usesOtel as boolean,
has_existing_observability_tool: orgDetails?.usesObservability as boolean,
existing_observability_tool:
orgDetails?.observabilityTool === 'Others'
? (orgDetails?.otherTool as string)
: (orgDetails?.observabilityTool as string),
where_did_you_discover_signoz: signozDetails?.discoverSignoz as string,
timeline_for_migrating_to_signoz: orgDetails?.migrationTimeline as string,
reasons_for_interest_in_signoz: signozDetails?.interestInSignoz?.includes(
'Others',
)
? ([
...(signozDetails?.interestInSignoz?.filter(
(item) => item !== 'Others',
) || []),
signozDetails?.otherInterestInSignoz,
] as string[])
: (signozDetails?.interestInSignoz as string[]),
logs_scale_per_day_in_gb: optimiseSignozDetails?.logsPerDay as number,
number_of_hosts: optimiseSignozDetails?.hostsPerDay as number,
number_of_services: optimiseSignozDetails?.services as number,
},
},
{
onSuccess: () => {
setCurrentStep(4);
},
onError: (error: any) => {
toast.error(error?.message || SOMETHING_WENT_WRONG);
// Allow user to proceed even if API fails
setCurrentStep(4);
},
},
);
};
const handleOnboardingComplete = (): void => {

View File

@@ -86,9 +86,9 @@ export default function OnboardingIngestionDetails(): JSX.Element {
<div className="ingestion-endpoint-section-error-container">
<Typography.Text className="ingestion-endpoint-section-error-text error">
<TriangleAlert size={14} />{' '}
{(error as AxiosError<RenderErrorResponseDTO>)?.response?.data?.error
?.message ||
(error as AxiosError)?.message ||
{((error as unknown) as AxiosError<RenderErrorResponseDTO>)?.response
?.data?.error.message ||
((error as unknown) as AxiosError)?.message ||
'Something went wrong'}
</Typography.Text>

View File

@@ -110,7 +110,7 @@ function AuthDomain(): JSX.Element {
let errorResult: APIError | null = null;
try {
ErrorResponseHandlerV2(
errorFetchingAuthDomainListResponse as AxiosError<ErrorV2Resp>,
(errorFetchingAuthDomainListResponse as unknown) as AxiosError<ErrorV2Resp>,
);
} catch (error) {
errorResult = error as APIError;

View File

@@ -1,10 +1,13 @@
import { CategoryNames } from 'container/NewWidget/RightContainer/types';
import { YAxisCategoryNames } from 'components/YAxisUnitSelector/constants';
export const categoryToSupport = [
CategoryNames.Data,
CategoryNames.DataRate,
CategoryNames.Time,
CategoryNames.Throughput,
CategoryNames.Miscellaneous,
CategoryNames.Boolean,
export const categoryToSupport: YAxisCategoryNames[] = [
YAxisCategoryNames.None,
YAxisCategoryNames.Data,
YAxisCategoryNames.DataRate,
YAxisCategoryNames.Time,
YAxisCategoryNames.Count,
YAxisCategoryNames.Operations,
YAxisCategoryNames.Percentage,
YAxisCategoryNames.Miscellaneous,
YAxisCategoryNames.Boolean,
];

View File

@@ -3,6 +3,7 @@ import { MemoryRouter, Route } from 'react-router-dom';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ROUTES from 'constants/routes';
import { SPAN_ATTRIBUTES } from 'container/ApiMonitoring/Explorer/Domains/DomainDetails/constants';
import { AppProvider } from 'providers/App/App';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { Span } from 'types/api/trace/getTraceV2';
@@ -108,7 +109,7 @@ const createMockSpan = (): Span => ({
statusMessage: '',
tagMap: {
'http.method': 'GET',
'http.url': '/api/users?page=1',
[SPAN_ATTRIBUTES.HTTP_URL]: '/api/users?page=1',
'http.status_code': '200',
'service.name': 'frontend-service',
'span.kind': 'server',

View File

@@ -5,6 +5,7 @@ import getSpanPercentiles from 'api/trace/getSpanPercentiles';
import getUserPreference from 'api/v1/user/preferences/name/get';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { SPAN_ATTRIBUTES } from 'container/ApiMonitoring/Explorer/Domains/DomainDetails/constants';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { server } from 'mocks-server/server';
import { QueryBuilderContext } from 'providers/QueryBuilder';
@@ -878,7 +879,9 @@ describe('SpanDetailsDrawer', () => {
// Verify only matching attributes are shown (use getAllByText for all since they appear in multiple places)
expect(screen.getAllByText('http.method').length).toBeGreaterThan(0);
expect(screen.getAllByText('http.url').length).toBeGreaterThan(0);
expect(screen.getAllByText(SPAN_ATTRIBUTES.HTTP_URL).length).toBeGreaterThan(
0,
);
expect(screen.getAllByText('http.status_code').length).toBeGreaterThan(0);
});
@@ -1126,7 +1129,7 @@ describe('SpanDetailsDrawer - Search Visibility User Flows', () => {
// User sees all attributes initially
expect(screen.getByText('http.method')).toBeInTheDocument();
expect(screen.getByText('http.url')).toBeInTheDocument();
expect(screen.getByText(SPAN_ATTRIBUTES.HTTP_URL)).toBeInTheDocument();
expect(screen.getByText('http.status_code')).toBeInTheDocument();
// User types "method" in search
@@ -1136,7 +1139,7 @@ describe('SpanDetailsDrawer - Search Visibility User Flows', () => {
// User sees only matching attributes
await waitFor(() => {
expect(screen.getByText('http.method')).toBeInTheDocument();
expect(screen.queryByText('http.url')).not.toBeInTheDocument();
expect(screen.queryByText(SPAN_ATTRIBUTES.HTTP_URL)).not.toBeInTheDocument();
expect(screen.queryByText('http.status_code')).not.toBeInTheDocument();
});
});

View File

@@ -1,3 +1,4 @@
import { SPAN_ATTRIBUTES } from 'container/ApiMonitoring/Explorer/Domains/DomainDetails/constants';
import { ILog } from 'types/api/logs/log';
import { Span } from 'types/api/trace/getTraceV2';
@@ -22,7 +23,7 @@ export const mockSpan: Span = {
event: [],
tagMap: {
'http.method': 'GET',
'http.url': '/api/test',
[SPAN_ATTRIBUTES.HTTP_URL]: '/api/test',
'http.status_code': '200',
},
hasError: false,

View File

@@ -1,13 +0,0 @@
import { useQuery, UseQueryResult } from 'react-query';
import { getDeploymentsData } from 'api/customDomain/getDeploymentsData';
import { AxiosError, AxiosResponse } from 'axios';
import { DeploymentsDataProps } from 'types/api/customDomain/types';
export const useGetDeploymentsData = (
isEnabled: boolean,
): UseQueryResult<AxiosResponse<DeploymentsDataProps>, AxiosError> =>
useQuery<AxiosResponse<DeploymentsDataProps>, AxiosError>({
queryKey: ['getDeploymentsData'],
queryFn: () => getDeploymentsData(),
enabled: isEnabled,
});

View File

@@ -1,15 +0,0 @@
import { useQuery, UseQueryResult } from 'react-query';
import { getAllIngestionKeys } from 'api/IngestionKeys/getAllIngestionKeys';
import { AxiosError, AxiosResponse } from 'axios';
import {
AllIngestionKeyProps,
GetIngestionKeyProps,
} from 'types/api/ingestionKeys/types';
export const useGetAllIngestionsKeys = (
props: GetIngestionKeyProps,
): UseQueryResult<AxiosResponse<AllIngestionKeyProps>, AxiosError> =>
useQuery<AxiosResponse<AllIngestionKeyProps>, AxiosError>({
queryKey: [`IngestionKeys-${props.page}-${props.search}`],
queryFn: () => getAllIngestionKeys(props),
});

View File

@@ -1,3 +1,5 @@
import { SPAN_ATTRIBUTES } from 'container/ApiMonitoring/Explorer/Domains/DomainDetails/constants';
/* eslint-disable sonarjs/no-duplicate-string */
export const traceDetailResponse = [
{
@@ -35,7 +37,7 @@ export const traceDetailResponse = [
'component',
'host.name',
'http.method',
'http.url',
SPAN_ATTRIBUTES.HTTP_URL,
'ip',
'http.status_code',
'opencensus.exporterversion',
@@ -84,7 +86,7 @@ export const traceDetailResponse = [
'signoz.collector.id',
'component',
'http.method',
'http.url',
SPAN_ATTRIBUTES.HTTP_URL,
'ip',
],
[
@@ -741,7 +743,7 @@ export const traceDetailResponse = [
'component',
'http.method',
'http.status_code',
'http.url',
SPAN_ATTRIBUTES.HTTP_URL,
'net/http.reused',
'net/http.was_idle',
'service.name',
@@ -833,7 +835,7 @@ export const traceDetailResponse = [
'opencensus.exporterversion',
'signoz.collector.id',
'host.name',
'http.url',
SPAN_ATTRIBUTES.HTTP_URL,
'net/http.reused',
'net/http.was_idle',
],
@@ -916,7 +918,7 @@ export const traceDetailResponse = [
'net/http.was_idle',
'component',
'host.name',
'http.url',
SPAN_ATTRIBUTES.HTTP_URL,
'ip',
'service.name',
'signoz.collector.id',

View File

@@ -2,6 +2,7 @@
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY } from 'constants/queryBuilder';
import { SPAN_ATTRIBUTES } from 'container/ApiMonitoring/Explorer/Domains/DomainDetails/constants';
import {
BaseAutocompleteData,
DataTypes,
@@ -31,7 +32,7 @@ export const AllTraceFilterKeyValue: Record<string, string> = {
httpRoute: 'HTTP Route',
'http.route': 'HTTP Route',
httpUrl: 'HTTP URL',
'http.url': 'HTTP URL',
[SPAN_ATTRIBUTES.HTTP_URL]: 'HTTP URL',
traceID: 'Trace ID',
trace_id: 'Trace ID',
} as const;

View File

@@ -1,53 +0,0 @@
export interface HostsProps {
name: string;
is_default: boolean;
}
export interface RegionProps {
id: string;
name: string;
category: string;
dns: string;
created_at: string;
updated_at: string;
}
export interface ClusterProps {
id: string;
name: string;
cloud_account_id: string;
cloud_region: string;
address: string;
region: RegionProps;
}
export interface DeploymentData {
id: string;
name: string;
email: string;
state: string;
tier: string;
user: string;
password: string;
created_at: string;
updated_at: string;
cluster_id: string;
hosts: HostsProps[];
cluster: ClusterProps;
}
export interface DeploymentsDataProps {
status: string;
data: DeploymentData;
}
export type PayloadProps = {
status: string;
data: string;
};
export interface UpdateCustomDomainProps {
data: {
name: string;
};
}

View File

@@ -1,11 +0,0 @@
export interface UpdateProfileProps {
reasons_for_interest_in_signoz: string[];
uses_otel: boolean;
has_existing_observability_tool: boolean;
existing_observability_tool: string;
logs_scale_per_day_in_gb: number;
number_of_services: number;
number_of_hosts: number;
where_did_you_discover_signoz: string;
timeline_for_migrating_to_signoz: string;
}

View File

@@ -0,0 +1,30 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/gorilla/mux"
)
func (provider *provider) addAuthzRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/authz/check", handler.New(provider.authzHandler.Check, handler.OpenAPIDef{
ID: "AuthzCheck",
Tags: []string{"authz"},
Summary: "Check permissions",
Description: "Checks if the authenticated user has permissions for given transactions",
Request: make([]*authtypes.Transaction, 0),
RequestContentType: "",
Response: make([]*authtypes.GettableTransaction, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: nil,
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -207,6 +207,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addAuthzRoutes(router); err != nil {
return err
}
if err := provider.addFieldsRoutes(router); err != nil {
return err
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/gorilla/mux"
)
@@ -15,12 +16,12 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Tags: []string{"role"},
Summary: "Create role",
Description: "This endpoint creates a role",
Request: nil,
Request: new(roletypes.PostableRole),
RequestContentType: "",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{},
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
@@ -44,6 +45,23 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/roles/resources", handler.New(provider.authZ.AdminAccess(provider.authzHandler.GetResources), handler.OpenAPIDef{
ID: "GetResources",
Tags: []string{"role"},
Summary: "Get resources",
Description: "Gets all the available resources for role assignment",
Request: nil,
RequestContentType: "",
Response: new(roletypes.GettableResources),
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/roles/{id}", handler.New(provider.authZ.AdminAccess(provider.authzHandler.Get), handler.OpenAPIDef{
ID: "GetRole",
Tags: []string{"role"},
@@ -61,17 +79,51 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/roles/{id}/relation/{relation}/objects", handler.New(provider.authZ.AdminAccess(provider.authzHandler.GetObjects), handler.OpenAPIDef{
ID: "GetObjects",
Tags: []string{"role"},
Summary: "Get objects for a role by relation",
Description: "Gets all objects connected to the specified role via a given relation type",
Request: nil,
RequestContentType: "",
Response: make([]*authtypes.Object, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authZ.AdminAccess(provider.authzHandler.Patch), handler.OpenAPIDef{
ID: "PatchRole",
Tags: []string{"role"},
Summary: "Patch role",
Description: "This endpoint patches a role",
Request: nil,
Request: new(roletypes.PatchableRole),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}/relation/{relation}/objects", handler.New(provider.authZ.AdminAccess(provider.authzHandler.PatchObjects), handler.OpenAPIDef{
ID: "PatchObjects",
Tags: []string{"role"},
Summary: "Patch objects for a role by relation",
Description: "Patches the objects connected to the specified role via a given relation type",
Request: new(roletypes.PatchableObjects),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPatch).GetError(); err != nil {
@@ -88,7 +140,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {

View File

@@ -14,17 +14,14 @@ import (
type AuthZ interface {
factory.Service
// Check returns error when the upstream authorization server is unavailable or the subject (s) doesn't have relation (r) on object (o).
Check(context.Context, *openfgav1.TupleKey) error
// CheckWithTupleCreation takes upon the responsibility for generating the tuples alongside everything Check does.
CheckWithTupleCreation(context.Context, authtypes.Claims, valuer.UUID, authtypes.Relation, authtypes.Typeable, []authtypes.Selector, []authtypes.Selector) error
// CheckWithTupleCreationWithoutClaims checks permissions for anonymous users.
CheckWithTupleCreationWithoutClaims(context.Context, valuer.UUID, authtypes.Relation, authtypes.Typeable, []authtypes.Selector, []authtypes.Selector) error
// Batch Check returns error when the upstream authorization server is unavailable or for all the tuples of subject (s) doesn't have relation (r) on object (o).
BatchCheck(context.Context, []*openfgav1.TupleKey) error
// BatchCheck accepts a map of ID → tuple and returns a map of ID → authorization result.
BatchCheck(context.Context, map[string]*openfgav1.TupleKey) (map[string]*authtypes.TupleKeyAuthorization, error)
// Write accepts the insertion tuples and the deletion tuples.
Write(context.Context, []*openfgav1.TupleKey, []*openfgav1.TupleKey) error
@@ -102,5 +99,7 @@ type Handler interface {
PatchObjects(http.ResponseWriter, *http.Request)
Check(http.ResponseWriter, *http.Request)
Delete(http.ResponseWriter, *http.Request)
}

View File

@@ -26,7 +26,7 @@ func (store *store) Create(ctx context.Context, role *roletypes.StorableRole) er
Model(role).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "role with id: %s already exists", role.ID)
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "role with name: %s already exists", role.Name)
}
return nil

View File

@@ -48,11 +48,7 @@ func (provider *provider) Stop(ctx context.Context) error {
return provider.server.Stop(ctx)
}
func (provider *provider) Check(ctx context.Context, tupleReq *openfgav1.TupleKey) error {
return provider.server.Check(ctx, tupleReq)
}
func (provider *provider) BatchCheck(ctx context.Context, tupleReq []*openfgav1.TupleKey) error {
func (provider *provider) BatchCheck(ctx context.Context, tupleReq map[string]*openfgav1.TupleKey) (map[string]*authtypes.TupleKeyAuthorization, error) {
return provider.server.BatchCheck(ctx, tupleReq)
}
@@ -181,10 +177,6 @@ func (provider *provider) CreateManagedRoles(ctx context.Context, _ valuer.UUID,
return nil
}
func (provider *provider) SetManagedRoleTransactions(context.Context, valuer.UUID) error {
return nil
}
func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) error {
return provider.Grant(ctx, orgID, roletypes.SigNozAdminRoleName, authtypes.MustNewSubject(authtypes.TypeableUser, userID.String(), orgID, nil))
}

View File

@@ -90,42 +90,18 @@ func (server *Server) Stop(ctx context.Context) error {
return nil
}
func (server *Server) Check(ctx context.Context, tupleReq *openfgav1.TupleKey) error {
func (server *Server) BatchCheck(ctx context.Context, tupleReq map[string]*openfgav1.TupleKey) (map[string]*authtypes.TupleKeyAuthorization, error) {
storeID, modelID := server.getStoreIDandModelID()
checkResponse, err := server.openfgaServer.Check(
ctx,
&openfgav1.CheckRequest{
StoreId: storeID,
AuthorizationModelId: modelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: tupleReq.User,
Relation: tupleReq.Relation,
Object: tupleReq.Object,
},
})
if err != nil {
return errors.Newf(errors.TypeInternal, authtypes.ErrCodeAuthZUnavailable, "authorization server is unavailable").WithAdditional(err.Error())
}
if !checkResponse.Allowed {
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "subject %s cannot %s object %s", tupleReq.User, tupleReq.Relation, tupleReq.Object)
}
return nil
}
func (server *Server) BatchCheck(ctx context.Context, tupleReq []*openfgav1.TupleKey) error {
storeID, modelID := server.getStoreIDandModelID()
batchCheckItems := make([]*openfgav1.BatchCheckItem, 0)
for idx, tuple := range tupleReq {
batchCheckItems := make([]*openfgav1.BatchCheckItem, 0, len(tupleReq))
for id, tuple := range tupleReq {
batchCheckItems = append(batchCheckItems, &openfgav1.BatchCheckItem{
TupleKey: &openfgav1.CheckRequestTupleKey{
User: tuple.User,
Relation: tuple.Relation,
Object: tuple.Object,
},
// the batch check response is map[string] keyed by correlationID.
CorrelationId: strconv.Itoa(idx),
// Use transaction ID as correlation ID for deterministic mapping
CorrelationId: id,
})
}
@@ -137,17 +113,18 @@ func (server *Server) BatchCheck(ctx context.Context, tupleReq []*openfgav1.Tupl
Checks: batchCheckItems,
})
if err != nil {
return errors.Newf(errors.TypeInternal, authtypes.ErrCodeAuthZUnavailable, "authorization server is unavailable").WithAdditional(err.Error())
return nil, errors.Newf(errors.TypeInternal, authtypes.ErrCodeAuthZUnavailable, "authorization server is unavailable").WithAdditional(err.Error())
}
for _, checkResponse := range checkResponse.Result {
if checkResponse.GetAllowed() {
return nil
response := make(map[string]*authtypes.TupleKeyAuthorization, len(tupleReq))
for id, tuple := range tupleReq {
response[id] = &authtypes.TupleKeyAuthorization{
Tuple: tuple,
Authorized: checkResponse.Result[id].GetAllowed(),
}
}
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "subjects are not authorized for requested access")
return response, nil
}
func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, _ authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector, roleSelectors []authtypes.Selector) error {
@@ -156,17 +133,29 @@ func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtyp
return err
}
tuples, err := authtypes.TypeableRole.Tuples(subject, authtypes.RelationAssignee, roleSelectors, orgID)
tupleSlice, err := authtypes.TypeableRole.Tuples(subject, authtypes.RelationAssignee, roleSelectors, orgID)
if err != nil {
return err
}
err = server.BatchCheck(ctx, tuples)
// Convert slice to map with generated IDs for internal use
tuples := make(map[string]*openfgav1.TupleKey, len(tupleSlice))
for idx, tuple := range tupleSlice {
tuples[strconv.Itoa(idx)] = tuple
}
response, err := server.BatchCheck(ctx, tuples)
if err != nil {
return err
}
return nil
for _, resp := range response {
if resp.Authorized {
return nil
}
}
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "subjects are not authorized for requested access")
}
func (server *Server) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, _ authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector, roleSelectors []authtypes.Selector) error {
@@ -175,17 +164,29 @@ func (server *Server) CheckWithTupleCreationWithoutClaims(ctx context.Context, o
return err
}
tuples, err := authtypes.TypeableRole.Tuples(subject, authtypes.RelationAssignee, roleSelectors, orgID)
tupleSlice, err := authtypes.TypeableRole.Tuples(subject, authtypes.RelationAssignee, roleSelectors, orgID)
if err != nil {
return err
}
err = server.BatchCheck(ctx, tuples)
// Convert slice to map with generated IDs for internal use
tuples := make(map[string]*openfgav1.TupleKey, len(tupleSlice))
for idx, tuple := range tupleSlice {
tuples[strconv.Itoa(idx)] = tuple
}
response, err := server.BatchCheck(ctx, tuples)
if err != nil {
return err
}
return nil
for _, resp := range response {
if resp.Authorized {
return nil
}
}
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "subjects are not authorized for requested access")
}
func (server *Server) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {

View File

@@ -7,6 +7,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -35,13 +36,14 @@ func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
return
}
err = handler.authz.Create(ctx, valuer.MustNewUUID(claims.OrgID), roletypes.NewRole(req.Name, req.Description, roletypes.RoleTypeCustom, valuer.MustNewUUID(claims.OrgID)))
role := roletypes.NewRole(req.Name, req.Description, roletypes.RoleTypeCustom, valuer.MustNewUUID(claims.OrgID))
err = handler.authz.Create(ctx, valuer.MustNewUUID(claims.OrgID), role)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, nil)
render.Success(rw, http.StatusCreated, types.Identifiable{ID: role.ID})
}
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
@@ -112,17 +114,9 @@ func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) {
}
func (handler *handler) GetResources(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
resources := handler.authz.GetResources(ctx)
resources := handler.authz.GetResources(r.Context())
var resourceRelations = struct {
Resources []*authtypes.Resource `json:"resources"`
Relations map[authtypes.Type][]authtypes.Relation `json:"relations"`
}{
Resources: resources,
Relations: authtypes.TypeableRelations,
}
render.Success(rw, http.StatusOK, resourceRelations)
render.Success(rw, http.StatusOK, roletypes.NewGettableResources(resources))
}
func (handler *handler) List(rw http.ResponseWriter, r *http.Request) {
@@ -227,7 +221,7 @@ func (handler *handler) PatchObjects(rw http.ResponseWriter, r *http.Request) {
return
}
render.Success(rw, http.StatusAccepted, nil)
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
@@ -252,3 +246,39 @@ func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) Check(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
transactions := make([]*authtypes.Transaction, 0)
if err := binding.JSON.BindBody(r.Body, &transactions); err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
subject, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
if err != nil {
render.Error(rw, err)
return
}
tuples, err := authtypes.NewTuplesFromTransactions(transactions, subject, orgID)
if err != nil {
render.Error(rw, err)
return
}
results, err := handler.authz.BatchCheck(ctx, tuples)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, authtypes.NewGettableTransaction(transactions, results))
}

View File

@@ -6,8 +6,8 @@ import (
)
type JSON struct {
Code string `json:"code"`
Message string `json:"message"`
Code string `json:"code" required:"true"`
Message string `json:"message" required:"true"`
Url string `json:"url,omitempty"`
Errors []responseerroradditional `json:"errors,omitempty"`
}

View File

@@ -16,13 +16,13 @@ const (
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type SuccessResponse struct {
Status string `json:"status"`
Data interface{} `json:"data,omitempty"`
Status string `json:"status" required:"true"`
Data interface{} `json:"data,omitempty" required:"true"`
}
type ErrorResponse struct {
Status string `json:"status"`
Error *errors.JSON `json:"error"`
Status string `json:"status" required:"true"`
Error *errors.JSON `json:"error" required:"true"`
}
func Success(rw http.ResponseWriter, httpCode int, data interface{}) {

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