mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-07 23:20:37 +01:00
Compare commits
64 Commits
nv/str-equ
...
fix/dashbo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8b7c330cb | ||
|
|
a949bacc90 | ||
|
|
90e0cb5bb5 | ||
|
|
cec72e25fd | ||
|
|
fbd2272bef | ||
|
|
a5d81f5b56 | ||
|
|
277ba6718f | ||
|
|
744351753b | ||
|
|
b8ac334b3c | ||
|
|
401b3ba98a | ||
|
|
30e3a72438 | ||
|
|
e4694f6411 | ||
|
|
2a1b1516d0 | ||
|
|
91fca0b395 | ||
|
|
256289614b | ||
|
|
d1b99c203f | ||
|
|
4bbef56d00 | ||
|
|
bee2b01d91 | ||
|
|
fcd7196f37 | ||
|
|
b66b8167d6 | ||
|
|
6e74eaee74 | ||
|
|
cc69ada22c | ||
|
|
3ca7b6da89 | ||
|
|
bf82650c9d | ||
|
|
78720ff018 | ||
|
|
49e5b61362 | ||
|
|
695d50c270 | ||
|
|
82c833044d | ||
|
|
f98b8bb1fd | ||
|
|
2114fbddfe | ||
|
|
b2214cf5a3 | ||
|
|
db4219e0f1 | ||
|
|
165c945511 | ||
|
|
ff19aedaa8 | ||
|
|
ef8319585b | ||
|
|
14ba332935 | ||
|
|
fda7b66d49 | ||
|
|
de2212e8b9 | ||
|
|
ff4f18985c | ||
|
|
7d555b643a | ||
|
|
7839e806fd | ||
|
|
2dcd51391d | ||
|
|
6251616d9d | ||
|
|
652a9044f4 | ||
|
|
d4646657ca | ||
|
|
29681572db | ||
|
|
a3fa475e88 | ||
|
|
64d0ada495 | ||
|
|
60886fd906 | ||
|
|
d8954da307 | ||
|
|
7476a2cacb | ||
|
|
e1dd7d52eb | ||
|
|
1644e35d9c | ||
|
|
379331ee77 | ||
|
|
4d079a8338 | ||
|
|
91938925ec | ||
|
|
e2588e6fff | ||
|
|
832a6cc794 | ||
|
|
5948b5b970 | ||
|
|
84834b087b | ||
|
|
d490126162 | ||
|
|
526a7d1c46 | ||
|
|
9f72338c87 | ||
|
|
e7ef4c6bdb |
@@ -155,8 +155,8 @@ querier:
|
||||
cache_ttl: 168h
|
||||
# The interval for recent data that should not be cached.
|
||||
flux_interval: 5m
|
||||
# The maximum number of concurrent queries for missing ranges.
|
||||
max_concurrent_queries: 4
|
||||
# The maximum number of queries a single query range request runs at once.
|
||||
max_concurrent_queries: 8
|
||||
# When filtering logs by trace_id, clamp the query window to the trace time
|
||||
# range with padding to include slightly delayed log exports. Logs only; set
|
||||
# to 0 to disable.
|
||||
|
||||
@@ -515,6 +515,13 @@ components:
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
AuthtypesDeprecatedPostableUserRole:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
AuthtypesGettableAuthDomain:
|
||||
properties:
|
||||
authNProviderInfo:
|
||||
@@ -660,17 +667,20 @@ components:
|
||||
type: string
|
||||
userRoles:
|
||||
items:
|
||||
$ref: '#/components/schemas/AuthtypesPostableUserRole'
|
||||
$ref: '#/components/schemas/AuthtypesDeprecatedPostableUserRole'
|
||||
type: array
|
||||
required:
|
||||
- email
|
||||
type: object
|
||||
AuthtypesPostableUserRole:
|
||||
properties:
|
||||
id:
|
||||
roleId:
|
||||
type: string
|
||||
userId:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- userId
|
||||
- roleId
|
||||
type: object
|
||||
AuthtypesRelation:
|
||||
enum:
|
||||
@@ -2917,6 +2927,13 @@ components:
|
||||
publicDashboard:
|
||||
$ref: '#/components/schemas/DashboardtypesGettablePublicDasbhboard'
|
||||
type: object
|
||||
DashboardtypesGettablePublicDashboardDataV2:
|
||||
properties:
|
||||
dashboard:
|
||||
$ref: '#/components/schemas/DashboardtypesGettableDashboardV2'
|
||||
publicDashboard:
|
||||
$ref: '#/components/schemas/DashboardtypesGettablePublicDasbhboard'
|
||||
type: object
|
||||
DashboardtypesHistogramBuckets:
|
||||
properties:
|
||||
bucketCount:
|
||||
@@ -4244,6 +4261,8 @@ components:
|
||||
$ref: '#/components/schemas/InframonitoringtypesNodeCountsByReadiness'
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
required:
|
||||
- clusterName
|
||||
- clusterCPU
|
||||
@@ -4252,6 +4271,7 @@ components:
|
||||
- clusterMemoryAllocatable
|
||||
- nodeCountsByReadiness
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesClusters:
|
||||
@@ -4307,6 +4327,8 @@ components:
|
||||
type: object
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
required:
|
||||
- daemonSetName
|
||||
- daemonSetCPU
|
||||
@@ -4318,6 +4340,7 @@ components:
|
||||
- desiredNodes
|
||||
- currentNodes
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesDaemonSets:
|
||||
@@ -4373,6 +4396,8 @@ components:
|
||||
type: object
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
required:
|
||||
- deploymentName
|
||||
- deploymentCPU
|
||||
@@ -4384,6 +4409,7 @@ components:
|
||||
- desiredPods
|
||||
- availablePods
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesDeployments:
|
||||
@@ -4516,6 +4542,8 @@ components:
|
||||
type: object
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
successfulPods:
|
||||
type: integer
|
||||
required:
|
||||
@@ -4531,6 +4559,7 @@ components:
|
||||
- failedPods
|
||||
- successfulPods
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesJobs:
|
||||
@@ -4621,11 +4650,14 @@ components:
|
||||
type: string
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
required:
|
||||
- namespaceName
|
||||
- namespaceCPU
|
||||
- namespaceMemory
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesNamespaces:
|
||||
@@ -4691,11 +4723,14 @@ components:
|
||||
type: string
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
required:
|
||||
- nodeName
|
||||
- condition
|
||||
- nodeCountsByReadiness
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- nodeCPU
|
||||
- nodeCPUAllocatable
|
||||
- nodeMemory
|
||||
@@ -4741,6 +4776,64 @@ components:
|
||||
- failed
|
||||
- unknown
|
||||
type: object
|
||||
InframonitoringtypesPodCountsByStatus:
|
||||
properties:
|
||||
completed:
|
||||
type: integer
|
||||
containerCannotRun:
|
||||
type: integer
|
||||
containerCreating:
|
||||
type: integer
|
||||
crashLoopBackOff:
|
||||
type: integer
|
||||
createContainerConfigError:
|
||||
type: integer
|
||||
errImagePull:
|
||||
type: integer
|
||||
error:
|
||||
type: integer
|
||||
evicted:
|
||||
type: integer
|
||||
failed:
|
||||
type: integer
|
||||
imagePullBackOff:
|
||||
type: integer
|
||||
nodeAffinity:
|
||||
type: integer
|
||||
nodeLost:
|
||||
type: integer
|
||||
oomKilled:
|
||||
type: integer
|
||||
pending:
|
||||
type: integer
|
||||
running:
|
||||
type: integer
|
||||
shutdown:
|
||||
type: integer
|
||||
unexpectedAdmissionError:
|
||||
type: integer
|
||||
unknown:
|
||||
type: integer
|
||||
required:
|
||||
- pending
|
||||
- running
|
||||
- failed
|
||||
- unknown
|
||||
- crashLoopBackOff
|
||||
- imagePullBackOff
|
||||
- errImagePull
|
||||
- createContainerConfigError
|
||||
- containerCreating
|
||||
- oomKilled
|
||||
- completed
|
||||
- error
|
||||
- containerCannotRun
|
||||
- evicted
|
||||
- nodeAffinity
|
||||
- nodeLost
|
||||
- shutdown
|
||||
- unexpectedAdmissionError
|
||||
type: object
|
||||
InframonitoringtypesPodPhase:
|
||||
enum:
|
||||
- pending
|
||||
@@ -4771,6 +4864,8 @@ components:
|
||||
type: number
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
podMemory:
|
||||
format: double
|
||||
type: number
|
||||
@@ -4782,6 +4877,11 @@ components:
|
||||
type: number
|
||||
podPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodPhase'
|
||||
podRestarts:
|
||||
format: int64
|
||||
type: integer
|
||||
podStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
|
||||
podUID:
|
||||
type: string
|
||||
required:
|
||||
@@ -4794,9 +4894,34 @@ components:
|
||||
- podMemoryLimit
|
||||
- podPhase
|
||||
- podCountsByPhase
|
||||
- podStatus
|
||||
- podCountsByStatus
|
||||
- podRestarts
|
||||
- podAge
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesPodStatus:
|
||||
enum:
|
||||
- pending
|
||||
- running
|
||||
- failed
|
||||
- unknown
|
||||
- crashloopbackoff
|
||||
- imagepullbackoff
|
||||
- errimagepull
|
||||
- createcontainerconfigerror
|
||||
- containercreating
|
||||
- oomkilled
|
||||
- completed
|
||||
- error
|
||||
- containercannotrun
|
||||
- evicted
|
||||
- nodeaffinity
|
||||
- nodelost
|
||||
- shutdown
|
||||
- unexpectedadmissionerror
|
||||
- no_data
|
||||
type: string
|
||||
InframonitoringtypesPods:
|
||||
properties:
|
||||
endTimeBeforeRetention:
|
||||
@@ -5095,6 +5220,8 @@ components:
|
||||
type: object
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
statefulSetCPU:
|
||||
format: double
|
||||
type: number
|
||||
@@ -5126,6 +5253,7 @@ components:
|
||||
- desiredPods
|
||||
- currentPods
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesStatefulSets:
|
||||
@@ -5224,6 +5352,16 @@ components:
|
||||
- offset
|
||||
- limit
|
||||
type: object
|
||||
LlmpricingruletypesGettableUnmappedModels:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesUnmappedModel'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- items
|
||||
type: object
|
||||
LlmpricingruletypesLLMPricingCacheCosts:
|
||||
properties:
|
||||
mode:
|
||||
@@ -5313,6 +5451,19 @@ components:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
LlmpricingruletypesUnmappedModel:
|
||||
properties:
|
||||
modelName:
|
||||
type: string
|
||||
provider:
|
||||
type: string
|
||||
spanCount:
|
||||
minimum: 0
|
||||
type: integer
|
||||
required:
|
||||
- modelName
|
||||
- spanCount
|
||||
type: object
|
||||
LlmpricingruletypesUpdatableLLMPricingRule:
|
||||
properties:
|
||||
enabled:
|
||||
@@ -7400,6 +7551,13 @@ components:
|
||||
enum:
|
||||
- basic
|
||||
type: string
|
||||
ServiceaccounttypesDeprecatedPostableServiceAccountRole:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
ServiceaccounttypesGettableFactorAPIKey:
|
||||
properties:
|
||||
createdAt:
|
||||
@@ -7456,10 +7614,13 @@ components:
|
||||
type: object
|
||||
ServiceaccounttypesPostableServiceAccountRole:
|
||||
properties:
|
||||
id:
|
||||
roleId:
|
||||
type: string
|
||||
serviceAccountId:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- serviceAccountId
|
||||
- roleId
|
||||
type: object
|
||||
ServiceaccounttypesServiceAccount:
|
||||
properties:
|
||||
@@ -11190,6 +11351,60 @@ paths:
|
||||
summary: Get a pricing rule
|
||||
tags:
|
||||
- llmpricingrules
|
||||
/api/v1/llm_pricing_rules/unmapped_models:
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns models seen in the last hour of trace data (gen_ai.request.model)
|
||||
that no pricing rule pattern matches, so the user can add them to an existing
|
||||
rule or create a new one.
|
||||
operationId: ListUnmappedLLMModels
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesGettableUnmappedModels'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: List unmapped models
|
||||
tags:
|
||||
- llmpricingrules
|
||||
/api/v1/logs/promote_paths:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -12168,6 +12383,188 @@ paths:
|
||||
summary: Update route policy
|
||||
tags:
|
||||
- routepolicies
|
||||
/api/v1/service_account_roles:
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint assigns a role to a service account
|
||||
operationId: CreateServiceAccountRole
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ServiceaccounttypesPostableServiceAccountRole'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$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:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- serviceaccount:attach
|
||||
- role:attach
|
||||
- tokenizer:
|
||||
- serviceaccount:attach
|
||||
- role:attach
|
||||
summary: Create service account role
|
||||
tags:
|
||||
- serviceaccount
|
||||
/api/v1/service_account_roles/{id}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: This endpoint revokes a role from a service account
|
||||
operationId: DeleteServiceAccountRole
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- serviceaccount:detach
|
||||
- role:detach
|
||||
- tokenizer:
|
||||
- serviceaccount:detach
|
||||
- role:detach
|
||||
summary: Delete service account role
|
||||
tags:
|
||||
- serviceaccount
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint gets an existing service account role
|
||||
operationId: GetServiceAccountRole
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/ServiceaccounttypesServiceAccountRole'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- serviceaccount:read
|
||||
- tokenizer:
|
||||
- serviceaccount:read
|
||||
summary: Get service account role
|
||||
tags:
|
||||
- serviceaccount
|
||||
/api/v1/service_accounts:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -12737,9 +13134,9 @@ paths:
|
||||
tags:
|
||||
- serviceaccount
|
||||
post:
|
||||
deprecated: false
|
||||
deprecated: true
|
||||
description: This endpoint assigns a role to a service account
|
||||
operationId: CreateServiceAccountRole
|
||||
operationId: CreateServiceAccountRoleDeprecated
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@@ -12750,7 +13147,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ServiceaccounttypesPostableServiceAccountRole'
|
||||
$ref: '#/components/schemas/ServiceaccounttypesDeprecatedPostableServiceAccountRole'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
@@ -12802,9 +13199,9 @@ paths:
|
||||
- serviceaccount
|
||||
/api/v1/service_accounts/{id}/roles/{rid}:
|
||||
delete:
|
||||
deprecated: false
|
||||
deprecated: true
|
||||
description: This endpoint revokes a role from service account
|
||||
operationId: DeleteServiceAccountRole
|
||||
operationId: DeleteServiceAccountRoleDeprecated
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@@ -17441,6 +17838,138 @@ paths:
|
||||
summary: Update my organization
|
||||
tags:
|
||||
- orgs
|
||||
/api/v2/public/dashboards/{id}:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns the sanitized v2-shape dashboard data for
|
||||
public access. Each panel query is reduced to a safe field subset, so filters
|
||||
and raw query strings are not exposed.
|
||||
operationId: GetPublicDashboardDataV2
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DashboardtypesGettablePublicDashboardDataV2'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- anonymous:
|
||||
- public-dashboard:read
|
||||
summary: Get public dashboard data (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/public/dashboards/{id}/panels/{key}/query_range:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns query range results for a panel of a v2-shape
|
||||
public dashboard. The panel is addressed by its key in spec.panels.
|
||||
operationId: GetPublicDashboardPanelQueryRangeV2
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: key
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeResponse'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- anonymous:
|
||||
- public-dashboard:read
|
||||
summary: Get query range result (v2)
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v2/readyz:
|
||||
get:
|
||||
operationId: Readyz
|
||||
@@ -21774,6 +22303,184 @@ paths:
|
||||
summary: Rotate session
|
||||
tags:
|
||||
- sessions
|
||||
/api/v2/user_roles:
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint assigns a role to a user
|
||||
operationId: CreateUserRole
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthtypesPostableUserRole'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$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:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Create user role
|
||||
tags:
|
||||
- users
|
||||
/api/v2/user_roles/{id}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: This endpoint revokes a role from a user
|
||||
operationId: DeleteUserRole
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Delete user role
|
||||
tags:
|
||||
- users
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint gets an existing user role
|
||||
operationId: GetUserRole
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AuthtypesUserRole'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Get user role
|
||||
tags:
|
||||
- users
|
||||
/api/v2/users:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -22227,7 +22934,7 @@ paths:
|
||||
tags:
|
||||
- users
|
||||
post:
|
||||
deprecated: false
|
||||
deprecated: true
|
||||
description: This endpoint assigns the role to the user roles by user id
|
||||
operationId: SetRoleByUserID
|
||||
parameters:
|
||||
@@ -22278,7 +22985,7 @@ paths:
|
||||
- users
|
||||
/api/v2/users/{id}/roles/{roleId}:
|
||||
delete:
|
||||
deprecated: false
|
||||
deprecated: true
|
||||
description: This endpoint removes a role from the user by user id and role
|
||||
id
|
||||
operationId: RemoveUserRoleByUserIDAndRoleID
|
||||
|
||||
149
docs/contributing/go/authz.md
Normal file
149
docs/contributing/go/authz.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Authz
|
||||
|
||||
SigNoz uses [OpenFGA](https://openfga.dev/), a relationship-based access control (ReBAC) system, to authorize every request. Transactions are never attached to users directly — they are attached to **roles** (as relationship tuples in OpenFGA), and users or service accounts (principals) are made **assignees** of those roles. The central interface is `AuthZ` in [pkg/authz/authz.go](/pkg/authz/authz.go), backed by an embedded OpenFGA server in [pkg/authz/openfgaserver](/pkg/authz/openfgaserver/server.go).
|
||||
|
||||
As a feature author, you will rarely touch OpenFGA directly. You interact with two layers:
|
||||
|
||||
1. **The registries** in [pkg/types/coretypes](/pkg/types/coretypes) — where every resource, verb, and managed-role permission is declared in code.
|
||||
2. **The route wiring** in [pkg/apiserver/signozapiserver](/pkg/apiserver/signozapiserver) — where each route declares what resource it touches and which verb it needs, and the middleware turns that into a check.
|
||||
|
||||
## What are the building blocks?
|
||||
|
||||
### Type
|
||||
|
||||
A `Type` is the coarse FGA type of a resource. The set is finite and defined in [pkg/types/coretypes/registry_type.go](/pkg/types/coretypes/registry_type.go): `user`, `serviceaccount`, `anonymous`, `role`, `organization`, `metaresource`, and `telemetryresource`. Each type carries a selector regex (what a valid ID looks like) and the verbs allowed on it.
|
||||
|
||||
Almost every feature resource is a `metaresource` (dashboards, rules, pipelines, ...) or a `telemetryresource` (logs, traces, metrics). You should almost never need a new type.
|
||||
|
||||
### Verb
|
||||
|
||||
A `Verb` is the action being authorized. All verbs are defined in [pkg/types/coretypes/registry_verb.go](/pkg/types/coretypes/registry_verb.go): `create`, `read`, `update`, `delete`, `list`, `assignee`, `attach`, and `detach`.
|
||||
|
||||
- `assignee` is special: it is the membership relation between a subject and a role ("user X is an assignee of role Y"), not an action a route checks for.
|
||||
- `attach`/`detach` authorize linking two resources together (e.g. assigning a role to a service account).
|
||||
|
||||
### Kind
|
||||
|
||||
A `Kind` is the fine-grained name of your resource — `dashboard`, `rule`, `quick-filter`. Kinds are registered in [pkg/types/coretypes/registry_kind.go](/pkg/types/coretypes/registry_kind.go). A kind rides on top of an existing type, so adding one does **not** require any OpenFGA schema change.
|
||||
|
||||
### Resource
|
||||
|
||||
A `Resource` ties a type and a kind together and knows how to render itself as an FGA object string. The interface lives in [pkg/types/coretypes/resource.go](/pkg/types/coretypes/resource.go):
|
||||
|
||||
```go
|
||||
type Resource interface {
|
||||
Type() Type
|
||||
Kind() Kind
|
||||
Prefix(orgId valuer.UUID) string // metaresource:organization/<orgID>/dashboard
|
||||
Object(orgId valuer.UUID, selector string) string
|
||||
Scope(verb Verb) string // dashboard:read
|
||||
AllowedVerbs() []Verb
|
||||
}
|
||||
```
|
||||
|
||||
All resources are registered in [pkg/types/coretypes/registry_resource.go](/pkg/types/coretypes/registry_resource.go) using constructors like `NewResourceMetaResource(KindDashboard)` or `NewResourceTelemetryResource(KindLogs)`.
|
||||
|
||||
### Selector
|
||||
|
||||
A `Selector` identifies *which* instance(s) of a resource a check is about — a UUID, a role name, or the wildcard `*`. A `SelectorFunc` maps the extracted resource ID to selectors at request time. Two prebuilt ones cover most routes ([pkg/types/coretypes/selector.go](/pkg/types/coretypes/selector.go)):
|
||||
|
||||
- `WildcardSelector` — the check is against all instances of the resource (`create`, `list`).
|
||||
- `IDSelector` — the check is against the specific instance *or* the wildcard (`read`, `update`, `delete` of one object). A subject authorized on `*` is authorized on every instance.
|
||||
|
||||
When the ID in the request is not what FGA needs (e.g. routes receive a role UUID but FGA objects use role names), write a custom `SelectorFunc` — see `roleSelector` in [pkg/apiserver/signozapiserver/serviceaccount.go](/pkg/apiserver/signozapiserver/serviceaccount.go).
|
||||
|
||||
### Roles, transactions, and tuples
|
||||
|
||||
SigNoz ships four managed roles, declared in [pkg/types/coretypes/registry_managed_role.go](/pkg/types/coretypes/registry_managed_role.go): `signoz-admin`, `signoz-editor`, `signoz-viewer`, and `signoz-anonymous`. Their permissions are declared in code as `Transaction`s (a verb on an object) in `ManagedRoleToTransactions` — this map is the single source of truth for what each managed role can do.
|
||||
|
||||
At organization bootstrap, `CreateManagedRoles` and `CreateManagedUserRoleTransactions` (see [pkg/authz/authz.go](/pkg/authz/authz.go)) persist the role rows and write one OpenFGA tuple per transaction, linking `role:organization/<orgID>/role/<name>#assignee` to each permitted object. Users and service accounts are then granted roles via `Grant`/`Revoke`, which writes `assignee` tuples. Custom roles (enterprise) are managed through the roles API in [pkg/authz/signozauthzapi/handler.go](/pkg/authz/signozauthzapi/handler.go).
|
||||
|
||||
### Schema
|
||||
|
||||
The OpenFGA authorization model is a hand-written DSL, embedded at build time: [pkg/authz/openfgaschema/base.fga](/pkg/authz/openfgaschema/base.fga) for community and [ee/authz/openfgaschema/base.fga](/ee/authz/openfgaschema/base.fga) for enterprise. The community model only supports role assignment; the enterprise model defines per-verb relations on every type, enabling genuine per-resource checks. This split is why `CheckWithTupleCreation` behaves differently per edition (see [How does a check work at runtime?](#how-does-a-check-work-at-runtime)). You only touch these files when introducing a brand-new **type** — never for a new kind.
|
||||
|
||||
## How do I add authz to my feature?
|
||||
|
||||
### 1. Register the kind
|
||||
|
||||
Add your kind in [pkg/types/coretypes/registry_kind.go](/pkg/types/coretypes/registry_kind.go) and append it to `Kinds`:
|
||||
|
||||
```go
|
||||
KindThing = MustNewKind("thing")
|
||||
```
|
||||
|
||||
### 2. Register the resource
|
||||
|
||||
Add the resource in [pkg/types/coretypes/registry_resource.go](/pkg/types/coretypes/registry_resource.go) and append it to `Resources`:
|
||||
|
||||
```go
|
||||
ResourceMetaResourceThing = NewResourceMetaResource(KindThing)
|
||||
```
|
||||
|
||||
Pass an explicit verb list to `NewResourceMetaResource` only if your resource supports fewer verbs than the type default.
|
||||
|
||||
### 3. Grant permissions to managed roles
|
||||
|
||||
Decide what each managed role can do with your resource and add the transactions in [pkg/types/coretypes/registry_managed_role.go](/pkg/types/coretypes/registry_managed_role.go):
|
||||
|
||||
```go
|
||||
// thing — editors manage, viewers read
|
||||
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindThing}, WildCardSelectorString)},
|
||||
```
|
||||
|
||||
The convention so far: admin gets everything, editor gets CRUD on day-to-day observability resources, viewer gets `read`/`list`, anonymous gets nothing (except public dashboards).
|
||||
|
||||
### 4. Wire the route
|
||||
|
||||
Register the route in [pkg/apiserver/signozapiserver](/pkg/apiserver/signozapiserver), wrapping your handler with `CheckResources` and declaring what the route touches via a `ResourceDef` ([pkg/http/handler/resourcedef.go](/pkg/http/handler/resourcedef.go)). A complete example from [pkg/apiserver/signozapiserver/serviceaccount.go](/pkg/apiserver/signozapiserver/serviceaccount.go):
|
||||
|
||||
```go
|
||||
router.Handle("/api/v1/service_accounts", handler.New(
|
||||
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.Create, authtypes.SigNozAdminRoleName),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateServiceAccount",
|
||||
// ...
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbCreate)}),
|
||||
},
|
||||
handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceServiceAccount,
|
||||
Verb: coretypes.VerbCreate,
|
||||
Category: coretypes.ActionCategoryAccessControl,
|
||||
ID: coretypes.ResponseJSONPath("data.id"),
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}),
|
||||
)).Methods(http.MethodPost)
|
||||
```
|
||||
|
||||
The pieces:
|
||||
|
||||
- **`CheckResources(handlerFn, roles...)`** — the resource-aware authorization wrapper from [pkg/http/middleware/authz.go](/pkg/http/middleware/authz.go). The role list is the community-edition fallback: which managed roles may call this route when per-resource checks are unavailable.
|
||||
- **`ResourceDef`** — declares the resource, verb, audit category, how to extract the instance ID, and how to turn that ID into selectors. ID extractors live in [pkg/types/coretypes/extractor.go](/pkg/types/coretypes/extractor.go): `PathParam("id")`, `BodyJSONPath("data.id")`, `BodyJSONArray("ids")`, and `ResponseJSONPath("data.id")` for IDs only known after the handler runs (e.g. `create`).
|
||||
- **`SecuritySchemes`** — advertises the required scope (`resource.Scope(verb)`, e.g. `serviceaccount:create`) in the OpenAPI spec.
|
||||
|
||||
For routes that link two resources, use `AttachDetachSiblingResourceDef` (both sides are authz-checked, e.g. attaching a role to a service account requires `attach` on **both** the service account and the role). For parent-child routes (e.g. creating an API key under a service account), both sides are checked too, but with different verbs: declare a `BasicResourceDef` checking the child with `create`/`delete`, alongside an `AttachDetachParentChildResourceDef` checking the parent with `attach`/`detach` (within that def the child is only recorded for audit) — see the `/api/v1/service_accounts/{id}/keys` route in [pkg/apiserver/signozapiserver/serviceaccount.go](/pkg/apiserver/signozapiserver/serviceaccount.go).
|
||||
|
||||
Prefer `CheckResources` with a `ResourceDef` for anything resource-shaped. The older coarse gates `ViewAccess`/`EditAccess`/`AdminAccess` only check "does the caller hold one of these roles" and give up per-resource granularity; `OpenAccess` performs no authorization (authentication still applies); `CheckWithoutClaims` serves anonymous routes such as public dashboards.
|
||||
|
||||
### 5. Backfill existing organizations (only if needed)
|
||||
|
||||
Managed-role tuples are written from the registry at organization creation, so **new resources need no migration for new organizations**. If existing organizations must get the new permissions, add a migration in [pkg/sqlmigration](/pkg/sqlmigration) that inserts the tuples — see [083_add_role_crud_tuples.go](/pkg/sqlmigration/083_add_role_crud_tuples.go) for the pattern.
|
||||
|
||||
## How does a check work at runtime?
|
||||
|
||||
1. The resource middleware ([pkg/http/middleware/resource.go](/pkg/http/middleware/resource.go)) runs on every request. It reads the matched handler's `ResourceDef`s, extracts the resource IDs from path/body, and stores the resolved resources in the request context.
|
||||
2. `CheckResources` reads them back, runs each `SelectorFunc`, and calls `AuthZ.CheckWithTupleCreation(ctx, claims, orgID, relation, resource, selectors, roleSelectors)`.
|
||||
3. What happens next depends on the edition:
|
||||
- **Community** ([pkg/authz/openfgaserver/server.go](/pkg/authz/openfgaserver/server.go)) ignores the resource and selectors and only checks whether the subject is an `assignee` of one of the allowed roles — a plain role gate.
|
||||
- **Enterprise** ([ee/authz/openfgaserver/server.go](/ee/authz/openfgaserver/server.go)) builds tuples via `authtypes.NewTuples` — subject `user:organization/<orgID>/user/<userID>`, relation `create`, object `serviceaccount:organization/<orgID>/serviceaccount/*` — and batch-checks them against OpenFGA: genuine per-resource authorization, including custom roles.
|
||||
|
||||
Because both paths go through the same middleware and the same `ResourceDef` declarations, a route wired once works correctly in both editions.
|
||||
|
||||
## What should I remember?
|
||||
|
||||
- Declare authz in the registries ([pkg/types/coretypes](/pkg/types/coretypes)), not in migrations — tuples for new organizations are derived from code at bootstrap.
|
||||
- A new kind never needs an OpenFGA schema change; only a new type does, and then **both** [pkg/authz/openfgaschema/base.fga](/pkg/authz/openfgaschema/base.fga) and [ee/authz/openfgaschema/base.fga](/ee/authz/openfgaschema/base.fga) must be updated together.
|
||||
- Prefer `CheckResources` + `ResourceDef` over the coarse `ViewAccess`/`EditAccess`/`AdminAccess` gates for new routes.
|
||||
- Use `WildcardSelector` for `create`/`list`, `IDSelector` for instance operations, and a custom `SelectorFunc` when the request ID is not the FGA selector.
|
||||
- Linking routes check **both** sides: peers via `AttachDetachSiblingResourceDef` (same verb on both), parent-child via a `BasicResourceDef` on the child (`create`/`delete`) paired with an `AttachDetachParentChildResourceDef` on the parent (`attach`/`detach`).
|
||||
- Changing `ManagedRoleToTransactions` only affects organizations created afterwards — add a [pkg/sqlmigration](/pkg/sqlmigration) migration to backfill existing ones.
|
||||
@@ -11,6 +11,7 @@ We adhere to three primary style guides as our foundation:
|
||||
We **recommend** (almost enforce) reviewing these guides before contributing to the codebase. They provide valuable insights into writing idiomatic Go code and will help you understand our approach to backend development. In addition, we have a few additional rules that make certain areas stricter than the above which can be found in area-specific files in this package:
|
||||
|
||||
- [Abstractions](abstractions.md) - When to introduce new types and intermediate representations
|
||||
- [Authz](authz.md) - Authorization, roles, and access control
|
||||
- [Errors](errors.md) - Structured error handling
|
||||
- [Endpoint](endpoint.md) - HTTP endpoint patterns
|
||||
- [Flagger](flagger.md) - Feature flag patterns
|
||||
|
||||
@@ -519,7 +519,7 @@ func (module *module) getOrCreateAPIKey(ctx context.Context, orgID valuer.UUID,
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = module.serviceAccount.SetRoleByName(ctx, orgID, serviceAccount.ID, authtypes.SigNozViewerRoleName)
|
||||
_, err = module.serviceAccount.SetRoleByName(ctx, orgID, serviceAccount.ID, authtypes.SigNozViewerRoleName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ type module struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
querier querier.Querier
|
||||
licensing licensing.Licensing
|
||||
tagModule tag.Module
|
||||
}
|
||||
|
||||
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
|
||||
@@ -41,6 +42,7 @@ func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, an
|
||||
settings: scopedProviderSettings,
|
||||
querier: querier,
|
||||
licensing: licensing,
|
||||
tagModule: tagModule,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +134,55 @@ func (module *module) GetPublicWidgetQueryRange(ctx context.Context, id valuer.U
|
||||
return module.querier.QueryRange(ctx, dashboard.OrgID, query)
|
||||
}
|
||||
|
||||
func (module *module) GetDashboardByPublicIDV2(ctx context.Context, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
|
||||
storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags, err := module.tagModule.ListForResource(ctx, storableDashboard.OrgID, coretypes.KindDashboard, storableDashboard.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return storableDashboard.ToDashboardV2(tags)
|
||||
}
|
||||
|
||||
func (module *module) GetPublicWidgetQueryRangeV2(ctx context.Context, id valuer.UUID, panelKey, startTimeRaw, endTimeRaw string) (*querybuildertypesv5.QueryRangeResponse, error) {
|
||||
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
||||
instrumentationtypes.CodeNamespace: "dashboard",
|
||||
instrumentationtypes.CodeFunctionName: "GetPublicWidgetQueryRangeV2",
|
||||
})
|
||||
|
||||
storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// tags are not needed for query range.
|
||||
dashboard, err := storableDashboard.ToDashboardV2(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicDashboard, err := module.GetPublic(ctx, dashboard.OrgID, dashboard.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
startTime, endTime, err := publicDashboard.ResolveTimeRange(startTimeRaw, endTimeRaw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query, err := dashboard.GetPanelQuery(startTime, endTime, panelKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return module.querier.QueryRange(ctx, dashboard.OrgID, query)
|
||||
}
|
||||
|
||||
func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publicDashboard *dashboardtypes.PublicDashboard) error {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
|
||||
@@ -141,7 +141,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationServiceAccount(ctx context.Cont
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration service account: %w", err))
|
||||
}
|
||||
err = ah.Signoz.Modules.ServiceAccount.SetRoleByName(ctx, orgId, cloudIntegrationServiceAccount.ID, authtypes.SigNozViewerRoleName)
|
||||
_, err = ah.Signoz.Modules.ServiceAccount.SetRoleByName(ctx, orgId, cloudIntegrationServiceAccount.ID, authtypes.SigNozViewerRoleName)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration service account: %w", err))
|
||||
}
|
||||
|
||||
@@ -293,6 +293,8 @@
|
||||
// Forces subpath imports (@signozhq/ui/<component>) instead of the eagerly-loaded barrel
|
||||
"signoz/no-css-module-bracket-access": "warn",
|
||||
// Prevents bracket access on CSS modules (styles['kebab-case']) which fails with camelCaseOnly config
|
||||
"signoz/no-dashboard-fetch-outside-root": "error",
|
||||
// Forces useDashboardFetchRequired() outside the root V2 pages (allowlisted in overrides below)
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
@@ -558,6 +560,18 @@
|
||||
"rules": {
|
||||
"signoz/no-zustand-getstate-in-hooks": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
// Root V2 pages own the dashboard fetch lifecycle; useDashboardFetchRequired wraps it.
|
||||
// Everywhere else must use useDashboardFetchRequired().
|
||||
"files": [
|
||||
"src/pages/DashboardPageV2/DashboardPageV2.tsx",
|
||||
"src/pages/DashboardPageV2/PanelEditorPage/PanelEditorPage.tsx",
|
||||
"src/pages/DashboardPageV2/DashboardContainer/hooks/useDashboardFetchRequired.ts"
|
||||
],
|
||||
"rules": {
|
||||
"signoz/no-dashboard-fetch-outside-root": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ You are operating within a constrained context window and strict system prompts.
|
||||
- Never create barrel files.
|
||||
- When writing new css, prefer CSS Modules
|
||||
- Use ./docs/css-modules-guide.md as reference on how to write good CSS Modules.
|
||||
- When writing code that could need authorization checks, read ./src/lib/authz/README.md
|
||||
|
||||
3. FORCED VERIFICATION: Your internal tools mark file writes as successful even if the code does not compile. You are FORBIDDEN from reporting a task as complete until you have:
|
||||
- Run `pnpm tsgo --noEmit`
|
||||
|
||||
3
frontend/__mocks__/lib/env.ts
Normal file
3
frontend/__mocks__/lib/env.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const IS_DEV = false;
|
||||
export const IS_PROD = true;
|
||||
export const MODE = 'test';
|
||||
@@ -29,6 +29,7 @@ const config: Config.InitialOptions = {
|
||||
'^constants/env$': '<rootDir>/__mocks__/env.ts',
|
||||
'^src/constants/env$': '<rootDir>/__mocks__/env.ts',
|
||||
'^@signozhq/icons$': '<rootDir>/__mocks__/signozhqIconsMock.tsx',
|
||||
'^lib/env$': '<rootDir>/__mocks__/lib/env.ts',
|
||||
'^test-mocks/(.*)$': '<rootDir>/__mocks__/$1',
|
||||
'^react-syntax-highlighter/dist/esm/(.*)$':
|
||||
'<rootDir>/node_modules/react-syntax-highlighter/dist/cjs/$1',
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
"event-source-polyfill": "1.0.31",
|
||||
"eventemitter3": "5.0.1",
|
||||
"history": "4.10.1",
|
||||
"html-to-image": "1.11.13",
|
||||
"http-status-codes": "2.3.0",
|
||||
"i18next": "^21.6.12",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
|
||||
48
frontend/plugins/rules/no-dashboard-fetch-outside-root.mjs
Normal file
48
frontend/plugins/rules/no-dashboard-fetch-outside-root.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Rule: no-dashboard-fetch-outside-root
|
||||
*
|
||||
* `useDashboardFetch` owns the V2 dashboard fetch and exposes its loading/error/refetch
|
||||
* lifecycle. That lifecycle is a root-page concern: DashboardPageV2 and PanelEditorPage
|
||||
* gate their subtree on a resolved dashboard before mounting anything below them.
|
||||
*
|
||||
* Every other component therefore renders inside an already-loaded subtree and must use
|
||||
* `useDashboardFetchRequired`, which reuses the same react-query cache entry but guarantees
|
||||
* a non-undefined dashboard (throwing otherwise) — so consumers don't re-handle states the
|
||||
* root page already owns.
|
||||
*
|
||||
* This rule flags `useDashboardFetch` imports. It is disabled via an override in
|
||||
* .oxlintrc.json for the two root pages and for useDashboardFetchRequired.ts (which wraps it).
|
||||
*/
|
||||
|
||||
const FETCH_HOOK = 'useDashboardFetch';
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description:
|
||||
'Disallow useDashboardFetch outside the root dashboard pages; use useDashboardFetchRequired instead',
|
||||
category: 'Dashboard V2',
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
useRequired:
|
||||
'Use useDashboardFetchRequired() instead of useDashboardFetch — the latter is reserved for the root pages (DashboardPageV2, PanelEditorPage) that own the fetch lifecycle. Components below them render inside a loaded subtree and should assume a guaranteed dashboard.',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
node.specifiers.forEach((specifier) => {
|
||||
if (
|
||||
specifier.type === 'ImportSpecifier' &&
|
||||
specifier.imported.name === FETCH_HOOK
|
||||
) {
|
||||
context.report({ node: specifier, messageId: 'useRequired' });
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import noRawAbsolutePath from './rules/no-raw-absolute-path.mjs';
|
||||
import noAntdComponents from './rules/no-antd-components.mjs';
|
||||
import noSignozhqUiBarrel from './rules/no-signozhq-ui-barrel.mjs';
|
||||
import noCssModuleBracketAccess from './rules/no-css-module-bracket-access.mjs';
|
||||
import noDashboardFetchOutsideRoot from './rules/no-dashboard-fetch-outside-root.mjs';
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
@@ -25,5 +26,6 @@ export default {
|
||||
'no-antd-components': noAntdComponents,
|
||||
'no-signozhq-ui-barrel': noSignozhqUiBarrel,
|
||||
'no-css-module-bracket-access': noCssModuleBracketAccess,
|
||||
'no-dashboard-fetch-outside-root': noDashboardFetchOutsideRoot,
|
||||
},
|
||||
};
|
||||
|
||||
8
frontend/pnpm-lock.yaml
generated
8
frontend/pnpm-lock.yaml
generated
@@ -164,6 +164,9 @@ importers:
|
||||
history:
|
||||
specifier: 4.10.1
|
||||
version: 4.10.1
|
||||
html-to-image:
|
||||
specifier: 1.11.13
|
||||
version: 1.11.13
|
||||
http-status-codes:
|
||||
specifier: 2.3.0
|
||||
version: 2.3.0
|
||||
@@ -5451,6 +5454,9 @@ packages:
|
||||
resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==}
|
||||
engines: {node: '>=20.10'}
|
||||
|
||||
html-to-image@1.11.13:
|
||||
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
|
||||
|
||||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
|
||||
@@ -14495,6 +14501,8 @@ snapshots:
|
||||
|
||||
html-tags@5.1.0: {}
|
||||
|
||||
html-to-image@1.11.13: {}
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
http-proxy-agent@5.0.0:
|
||||
|
||||
@@ -62,6 +62,6 @@
|
||||
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
|
||||
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
|
||||
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
|
||||
"LLM_OBSERVABILITY_BASE": "SigNoz | LLM Observability",
|
||||
"LLM_OBSERVABILITY_MODEL_PRICING": "SigNoz | Model Pricing"
|
||||
}
|
||||
"LLM_OBSERVABILITY_OVERVIEW": "SigNoz | LLM Observability Overview",
|
||||
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration"
|
||||
}
|
||||
@@ -87,6 +87,6 @@
|
||||
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
|
||||
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
|
||||
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
|
||||
"LLM_OBSERVABILITY_BASE": "SigNoz | LLM Observability",
|
||||
"LLM_OBSERVABILITY_MODEL_PRICING": "SigNoz | Model Pricing"
|
||||
}
|
||||
"LLM_OBSERVABILITY_OVERVIEW": "SigNoz | LLM Observability Overview",
|
||||
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration"
|
||||
}
|
||||
@@ -329,10 +329,3 @@ export const LLMObservabilityPage = Loadable(
|
||||
/* webpackChunkName: "LLM Observability Page" */ 'pages/LLMObservability'
|
||||
),
|
||||
);
|
||||
|
||||
export const LLMObservabilityModelPricingPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "LLM Observability Model Pricing Page" */ 'pages/LLMObservabilityModelPricing'
|
||||
),
|
||||
);
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
LicensePage,
|
||||
ListAllALertsPage,
|
||||
LLMObservabilityPage,
|
||||
LLMObservabilityModelPricingPage,
|
||||
LiveLogs,
|
||||
Login,
|
||||
Logs,
|
||||
@@ -515,17 +514,17 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LLM_OBSERVABILITY_BASE,
|
||||
path: ROUTES.LLM_OBSERVABILITY_OVERVIEW,
|
||||
exact: true,
|
||||
component: LLMObservabilityPage,
|
||||
key: 'LLM_OBSERVABILITY_BASE',
|
||||
key: 'LLM_OBSERVABILITY_OVERVIEW',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LLM_OBSERVABILITY_MODEL_PRICING,
|
||||
path: ROUTES.LLM_OBSERVABILITY_CONFIGURATION,
|
||||
exact: true,
|
||||
component: LLMObservabilityModelPricingPage,
|
||||
key: 'LLM_OBSERVABILITY_MODEL_PRICING',
|
||||
component: LLMObservabilityPage,
|
||||
key: 'LLM_OBSERVABILITY_CONFIGURATION',
|
||||
isPrivate: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -38,6 +38,10 @@ import type {
|
||||
GetPublicDashboard200,
|
||||
GetPublicDashboardData200,
|
||||
GetPublicDashboardDataPathParameters,
|
||||
GetPublicDashboardDataV2200,
|
||||
GetPublicDashboardDataV2PathParameters,
|
||||
GetPublicDashboardPanelQueryRangeV2200,
|
||||
GetPublicDashboardPanelQueryRangeV2PathParameters,
|
||||
GetPublicDashboardPathParameters,
|
||||
GetPublicDashboardWidgetQueryRange200,
|
||||
GetPublicDashboardWidgetQueryRangePathParameters,
|
||||
@@ -1799,6 +1803,217 @@ export const useLockDashboardV2 = <
|
||||
> => {
|
||||
return useMutation(getLockDashboardV2MutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint returns the sanitized v2-shape dashboard data for public access. Each panel query is reduced to a safe field subset, so filters and raw query strings are not exposed.
|
||||
* @summary Get public dashboard data (v2)
|
||||
*/
|
||||
export const getPublicDashboardDataV2 = (
|
||||
{ id }: GetPublicDashboardDataV2PathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetPublicDashboardDataV2200>({
|
||||
url: `/api/v2/public/dashboards/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetPublicDashboardDataV2QueryKey = ({
|
||||
id,
|
||||
}: GetPublicDashboardDataV2PathParameters) => {
|
||||
return [`/api/v2/public/dashboards/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetPublicDashboardDataV2QueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetPublicDashboardDataV2PathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetPublicDashboardDataV2QueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getPublicDashboardDataV2>>
|
||||
> = ({ signal }) => getPublicDashboardDataV2({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetPublicDashboardDataV2QueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getPublicDashboardDataV2>>
|
||||
>;
|
||||
export type GetPublicDashboardDataV2QueryError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get public dashboard data (v2)
|
||||
*/
|
||||
|
||||
export function useGetPublicDashboardDataV2<
|
||||
TData = Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetPublicDashboardDataV2PathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetPublicDashboardDataV2QueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get public dashboard data (v2)
|
||||
*/
|
||||
export const invalidateGetPublicDashboardDataV2 = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetPublicDashboardDataV2PathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetPublicDashboardDataV2QueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint returns query range results for a panel of a v2-shape public dashboard. The panel is addressed by its key in spec.panels.
|
||||
* @summary Get query range result (v2)
|
||||
*/
|
||||
export const getPublicDashboardPanelQueryRangeV2 = (
|
||||
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetPublicDashboardPanelQueryRangeV2200>({
|
||||
url: `/api/v2/public/dashboards/${id}/panels/${key}/query_range`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetPublicDashboardPanelQueryRangeV2QueryKey = ({
|
||||
id,
|
||||
key,
|
||||
}: GetPublicDashboardPanelQueryRangeV2PathParameters) => {
|
||||
return [`/api/v2/public/dashboards/${id}/panels/${key}/query_range`] as const;
|
||||
};
|
||||
|
||||
export const getGetPublicDashboardPanelQueryRangeV2QueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ??
|
||||
getGetPublicDashboardPanelQueryRangeV2QueryKey({ id, key });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>
|
||||
> = ({ signal }) => getPublicDashboardPanelQueryRangeV2({ id, key }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!(id && key),
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetPublicDashboardPanelQueryRangeV2QueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>
|
||||
>;
|
||||
export type GetPublicDashboardPanelQueryRangeV2QueryError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get query range result (v2)
|
||||
*/
|
||||
|
||||
export function useGetPublicDashboardPanelQueryRangeV2<
|
||||
TData = Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetPublicDashboardPanelQueryRangeV2QueryOptions(
|
||||
{ id, key },
|
||||
options,
|
||||
);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get query range result (v2)
|
||||
*/
|
||||
export const invalidateGetPublicDashboardPanelQueryRangeV2 = async (
|
||||
queryClient: QueryClient,
|
||||
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetPublicDashboardPanelQueryRangeV2QueryKey({ id, key }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Same as ListDashboardsV2 but personalized for the calling user: each dashboard carries the caller's `pinned` state, and pinned dashboards float to the top of the requested ordering. Supports the same filter DSL, sort, order, and pagination.
|
||||
* @summary List dashboards for the current user (v2)
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
GetLLMPricingRulePathParameters,
|
||||
ListLLMPricingRules200,
|
||||
ListLLMPricingRulesParams,
|
||||
ListUnmappedLLMModels200,
|
||||
LlmpricingruletypesUpdatableLLMPricingRulesDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
@@ -393,3 +394,87 @@ export const invalidateGetLLMPricingRule = async (
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns models seen in the last hour of trace data (gen_ai.request.model) that no pricing rule pattern matches, so the user can add them to an existing rule or create a new one.
|
||||
* @summary List unmapped models
|
||||
*/
|
||||
export const listUnmappedLLMModels = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<ListUnmappedLLMModels200>({
|
||||
url: `/api/v1/llm_pricing_rules/unmapped_models`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListUnmappedLLMModelsQueryKey = () => {
|
||||
return [`/api/v1/llm_pricing_rules/unmapped_models`] as const;
|
||||
};
|
||||
|
||||
export const getListUnmappedLLMModelsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listUnmappedLLMModels>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListUnmappedLLMModelsQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof listUnmappedLLMModels>>
|
||||
> = ({ signal }) => listUnmappedLLMModels(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListUnmappedLLMModelsQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listUnmappedLLMModels>>
|
||||
>;
|
||||
export type ListUnmappedLLMModelsQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List unmapped models
|
||||
*/
|
||||
|
||||
export function useListUnmappedLLMModels<
|
||||
TData = Awaited<ReturnType<typeof listUnmappedLLMModels>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListUnmappedLLMModelsQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List unmapped models
|
||||
*/
|
||||
export const invalidateListUnmappedLLMModels = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListUnmappedLLMModelsQueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
@@ -22,12 +22,16 @@ import type {
|
||||
CreateServiceAccountKey201,
|
||||
CreateServiceAccountKeyPathParameters,
|
||||
CreateServiceAccountRole201,
|
||||
CreateServiceAccountRolePathParameters,
|
||||
CreateServiceAccountRoleDeprecated201,
|
||||
CreateServiceAccountRoleDeprecatedPathParameters,
|
||||
DeleteServiceAccountPathParameters,
|
||||
DeleteServiceAccountRoleDeprecatedPathParameters,
|
||||
DeleteServiceAccountRolePathParameters,
|
||||
GetMyServiceAccount200,
|
||||
GetServiceAccount200,
|
||||
GetServiceAccountPathParameters,
|
||||
GetServiceAccountRole200,
|
||||
GetServiceAccountRolePathParameters,
|
||||
GetServiceAccountRoles200,
|
||||
GetServiceAccountRolesPathParameters,
|
||||
ListServiceAccountKeys200,
|
||||
@@ -35,6 +39,7 @@ import type {
|
||||
ListServiceAccounts200,
|
||||
RenderErrorResponseDTO,
|
||||
RevokeServiceAccountKeyPathParameters,
|
||||
ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO,
|
||||
ServiceaccounttypesPostableFactorAPIKeyDTO,
|
||||
ServiceaccounttypesPostableServiceAccountDTO,
|
||||
ServiceaccounttypesPostableServiceAccountRoleDTO,
|
||||
@@ -46,6 +51,272 @@ import type {
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* This endpoint assigns a role to a service account
|
||||
* @summary Create service account role
|
||||
*/
|
||||
export const createServiceAccountRole = (
|
||||
serviceaccounttypesPostableServiceAccountRoleDTO?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateServiceAccountRole201>({
|
||||
url: `/api/v1/service_account_roles`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: serviceaccounttypesPostableServiceAccountRoleDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateServiceAccountRoleMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>,
|
||||
TError,
|
||||
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>,
|
||||
TError,
|
||||
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createServiceAccountRole'];
|
||||
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 createServiceAccountRole>>,
|
||||
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createServiceAccountRole(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateServiceAccountRoleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>
|
||||
>;
|
||||
export type CreateServiceAccountRoleMutationBody =
|
||||
| BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>
|
||||
| undefined;
|
||||
export type CreateServiceAccountRoleMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create service account role
|
||||
*/
|
||||
export const useCreateServiceAccountRole = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>,
|
||||
TError,
|
||||
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>,
|
||||
TError,
|
||||
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateServiceAccountRoleMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint revokes a role from a service account
|
||||
* @summary Delete service account role
|
||||
*/
|
||||
export const deleteServiceAccountRole = (
|
||||
{ id }: DeleteServiceAccountRolePathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/service_account_roles/${id}`,
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteServiceAccountRoleMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
|
||||
TError,
|
||||
{ pathParams: DeleteServiceAccountRolePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
|
||||
TError,
|
||||
{ pathParams: DeleteServiceAccountRolePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteServiceAccountRole'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
|
||||
{ pathParams: DeleteServiceAccountRolePathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteServiceAccountRole(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteServiceAccountRoleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>
|
||||
>;
|
||||
|
||||
export type DeleteServiceAccountRoleMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Delete service account role
|
||||
*/
|
||||
export const useDeleteServiceAccountRole = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
|
||||
TError,
|
||||
{ pathParams: DeleteServiceAccountRolePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
|
||||
TError,
|
||||
{ pathParams: DeleteServiceAccountRolePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getDeleteServiceAccountRoleMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint gets an existing service account role
|
||||
* @summary Get service account role
|
||||
*/
|
||||
export const getServiceAccountRole = (
|
||||
{ id }: GetServiceAccountRolePathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetServiceAccountRole200>({
|
||||
url: `/api/v1/service_account_roles/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetServiceAccountRoleQueryKey = ({
|
||||
id,
|
||||
}: GetServiceAccountRolePathParameters) => {
|
||||
return [`/api/v1/service_account_roles/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetServiceAccountRoleQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getServiceAccountRole>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetServiceAccountRolePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getServiceAccountRole>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetServiceAccountRoleQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getServiceAccountRole>>
|
||||
> = ({ signal }) => getServiceAccountRole({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getServiceAccountRole>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetServiceAccountRoleQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getServiceAccountRole>>
|
||||
>;
|
||||
export type GetServiceAccountRoleQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get service account role
|
||||
*/
|
||||
|
||||
export function useGetServiceAccountRole<
|
||||
TData = Awaited<ReturnType<typeof getServiceAccountRole>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetServiceAccountRolePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getServiceAccountRole>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetServiceAccountRoleQueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get service account role
|
||||
*/
|
||||
export const invalidateGetServiceAccountRole = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetServiceAccountRolePathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetServiceAccountRoleQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint lists the service accounts for an organisation
|
||||
* @summary List service accounts
|
||||
@@ -984,45 +1255,46 @@ export const invalidateGetServiceAccountRoles = async (
|
||||
|
||||
/**
|
||||
* This endpoint assigns a role to a service account
|
||||
* @deprecated
|
||||
* @summary Create service account role
|
||||
*/
|
||||
export const createServiceAccountRole = (
|
||||
{ id }: CreateServiceAccountRolePathParameters,
|
||||
serviceaccounttypesPostableServiceAccountRoleDTO?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>,
|
||||
export const createServiceAccountRoleDeprecated = (
|
||||
{ id }: CreateServiceAccountRoleDeprecatedPathParameters,
|
||||
serviceaccounttypesDeprecatedPostableServiceAccountRoleDTO?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateServiceAccountRole201>({
|
||||
return GeneratedAPIInstance<CreateServiceAccountRoleDeprecated201>({
|
||||
url: `/api/v1/service_accounts/${id}/roles`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: serviceaccounttypesPostableServiceAccountRoleDTO,
|
||||
data: serviceaccounttypesDeprecatedPostableServiceAccountRoleDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateServiceAccountRoleMutationOptions = <
|
||||
export const getCreateServiceAccountRoleDeprecatedMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>,
|
||||
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreateServiceAccountRolePathParameters;
|
||||
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
|
||||
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
|
||||
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>,
|
||||
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreateServiceAccountRolePathParameters;
|
||||
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
|
||||
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
|
||||
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createServiceAccountRole'];
|
||||
const mutationKey = ['createServiceAccountRoleDeprecated'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
@@ -1032,62 +1304,66 @@ export const getCreateServiceAccountRoleMutationOptions = <
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>,
|
||||
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
|
||||
{
|
||||
pathParams: CreateServiceAccountRolePathParameters;
|
||||
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
|
||||
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
|
||||
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return createServiceAccountRole(pathParams, data);
|
||||
return createServiceAccountRoleDeprecated(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateServiceAccountRoleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>
|
||||
export type CreateServiceAccountRoleDeprecatedMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>
|
||||
>;
|
||||
export type CreateServiceAccountRoleMutationBody =
|
||||
| BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>
|
||||
export type CreateServiceAccountRoleDeprecatedMutationBody =
|
||||
| BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>
|
||||
| undefined;
|
||||
export type CreateServiceAccountRoleMutationError =
|
||||
export type CreateServiceAccountRoleDeprecatedMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Create service account role
|
||||
*/
|
||||
export const useCreateServiceAccountRole = <
|
||||
export const useCreateServiceAccountRoleDeprecated = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>,
|
||||
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreateServiceAccountRolePathParameters;
|
||||
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
|
||||
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
|
||||
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createServiceAccountRole>>,
|
||||
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreateServiceAccountRolePathParameters;
|
||||
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
|
||||
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
|
||||
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateServiceAccountRoleMutationOptions(options));
|
||||
return useMutation(
|
||||
getCreateServiceAccountRoleDeprecatedMutationOptions(options),
|
||||
);
|
||||
};
|
||||
/**
|
||||
* This endpoint revokes a role from service account
|
||||
* @deprecated
|
||||
* @summary Delete service account role
|
||||
*/
|
||||
export const deleteServiceAccountRole = (
|
||||
{ id, rid }: DeleteServiceAccountRolePathParameters,
|
||||
export const deleteServiceAccountRoleDeprecated = (
|
||||
{ id, rid }: DeleteServiceAccountRoleDeprecatedPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
@@ -1097,23 +1373,23 @@ export const deleteServiceAccountRole = (
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteServiceAccountRoleMutationOptions = <
|
||||
export const getDeleteServiceAccountRoleDeprecatedMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
|
||||
TError,
|
||||
{ pathParams: DeleteServiceAccountRolePathParameters },
|
||||
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
|
||||
TError,
|
||||
{ pathParams: DeleteServiceAccountRolePathParameters },
|
||||
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteServiceAccountRole'];
|
||||
const mutationKey = ['deleteServiceAccountRoleDeprecated'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
@@ -1123,44 +1399,47 @@ export const getDeleteServiceAccountRoleMutationOptions = <
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
|
||||
{ pathParams: DeleteServiceAccountRolePathParameters }
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
|
||||
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteServiceAccountRole(pathParams);
|
||||
return deleteServiceAccountRoleDeprecated(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteServiceAccountRoleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>
|
||||
export type DeleteServiceAccountRoleDeprecatedMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>
|
||||
>;
|
||||
|
||||
export type DeleteServiceAccountRoleMutationError =
|
||||
export type DeleteServiceAccountRoleDeprecatedMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Delete service account role
|
||||
*/
|
||||
export const useDeleteServiceAccountRole = <
|
||||
export const useDeleteServiceAccountRoleDeprecated = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
|
||||
TError,
|
||||
{ pathParams: DeleteServiceAccountRolePathParameters },
|
||||
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
|
||||
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
|
||||
TError,
|
||||
{ pathParams: DeleteServiceAccountRolePathParameters },
|
||||
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getDeleteServiceAccountRoleMutationOptions(options));
|
||||
return useMutation(
|
||||
getDeleteServiceAccountRoleDeprecatedMutationOptions(options),
|
||||
);
|
||||
};
|
||||
/**
|
||||
* This endpoint gets my service account
|
||||
|
||||
@@ -2048,6 +2048,13 @@ export interface AuthtypesAuthNSupportDTO {
|
||||
password?: AuthtypesPasswordAuthNSupportDTO[] | null;
|
||||
}
|
||||
|
||||
export interface AuthtypesDeprecatedPostableUserRoleDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface AuthtypesGettableAuthDomainDTO {
|
||||
authNProviderInfo?: AuthtypesAuthNProviderInfoDTO;
|
||||
config?: AuthtypesAuthDomainConfigDTO;
|
||||
@@ -2287,13 +2294,6 @@ export interface AuthtypesPostableRotateTokenDTO {
|
||||
refreshToken?: string;
|
||||
}
|
||||
|
||||
export interface AuthtypesPostableUserRoleDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface AuthtypesPostableUserDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -2310,7 +2310,18 @@ export interface AuthtypesPostableUserDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
userRoles?: AuthtypesPostableUserRoleDTO[];
|
||||
userRoles?: AuthtypesDeprecatedPostableUserRoleDTO[];
|
||||
}
|
||||
|
||||
export interface AuthtypesPostableUserRoleDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
roleId: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface AuthtypesRoleDTO {
|
||||
@@ -4941,6 +4952,11 @@ export interface DashboardtypesGettablePublicDashboardDataDTO {
|
||||
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
|
||||
}
|
||||
|
||||
export interface DashboardtypesGettablePublicDashboardDataV2DTO {
|
||||
dashboard?: DashboardtypesGettableDashboardV2DTO;
|
||||
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
|
||||
}
|
||||
|
||||
export enum DashboardtypesPatchOpDTO {
|
||||
add = 'add',
|
||||
remove = 'remove',
|
||||
@@ -5688,6 +5704,81 @@ export interface InframonitoringtypesPodCountsByPhaseDTO {
|
||||
unknown: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPodCountsByStatusDTO {
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
completed: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
containerCannotRun: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
containerCreating: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
crashLoopBackOff: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
createContainerConfigError: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
errImagePull: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
error: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
evicted: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
failed: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
imagePullBackOff: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
nodeAffinity: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
nodeLost: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
oomKilled: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
pending: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
running: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
shutdown: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
unexpectedAdmissionError: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
unknown: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesClusterRecordDTO {
|
||||
/**
|
||||
* @type number
|
||||
@@ -5719,6 +5810,7 @@ export interface InframonitoringtypesClusterRecordDTO {
|
||||
meta: InframonitoringtypesClusterRecordDTOMeta;
|
||||
nodeCountsByReadiness: InframonitoringtypesNodeCountsByReadinessDTO;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesResponseTypeDTO {
|
||||
@@ -5822,6 +5914,7 @@ export interface InframonitoringtypesDaemonSetRecordDTO {
|
||||
*/
|
||||
meta: InframonitoringtypesDaemonSetRecordDTOMeta;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDaemonSetsDTO {
|
||||
@@ -5899,6 +5992,7 @@ export interface InframonitoringtypesDeploymentRecordDTO {
|
||||
*/
|
||||
meta: InframonitoringtypesDeploymentRecordDTOMeta;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDeploymentsDTO {
|
||||
@@ -6065,6 +6159,7 @@ export interface InframonitoringtypesJobRecordDTO {
|
||||
*/
|
||||
meta: InframonitoringtypesJobRecordDTOMeta;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -6118,6 +6213,7 @@ export interface InframonitoringtypesNamespaceRecordDTO {
|
||||
*/
|
||||
namespaceName: string;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesNamespacesDTO {
|
||||
@@ -6184,6 +6280,7 @@ export interface InframonitoringtypesNodeRecordDTO {
|
||||
*/
|
||||
nodeName: string;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesNodesDTO {
|
||||
@@ -6221,6 +6318,27 @@ export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
|
||||
export type InframonitoringtypesPodRecordDTOMeta =
|
||||
InframonitoringtypesPodRecordDTOMetaAnyOf | null;
|
||||
|
||||
export enum InframonitoringtypesPodStatusDTO {
|
||||
pending = 'pending',
|
||||
running = 'running',
|
||||
failed = 'failed',
|
||||
unknown = 'unknown',
|
||||
crashloopbackoff = 'crashloopbackoff',
|
||||
imagepullbackoff = 'imagepullbackoff',
|
||||
errimagepull = 'errimagepull',
|
||||
createcontainerconfigerror = 'createcontainerconfigerror',
|
||||
containercreating = 'containercreating',
|
||||
oomkilled = 'oomkilled',
|
||||
completed = 'completed',
|
||||
error = 'error',
|
||||
containercannotrun = 'containercannotrun',
|
||||
evicted = 'evicted',
|
||||
nodeaffinity = 'nodeaffinity',
|
||||
nodelost = 'nodelost',
|
||||
shutdown = 'shutdown',
|
||||
unexpectedadmissionerror = 'unexpectedadmissionerror',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export interface InframonitoringtypesPodRecordDTO {
|
||||
/**
|
||||
* @type object,null
|
||||
@@ -6247,6 +6365,7 @@ export interface InframonitoringtypesPodRecordDTO {
|
||||
*/
|
||||
podCPURequest: number;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
@@ -6263,6 +6382,12 @@ export interface InframonitoringtypesPodRecordDTO {
|
||||
*/
|
||||
podMemoryRequest: number;
|
||||
podPhase: InframonitoringtypesPodPhaseDTO;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
podRestarts: number;
|
||||
podStatus: InframonitoringtypesPodStatusDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -6580,6 +6705,7 @@ export interface InframonitoringtypesStatefulSetRecordDTO {
|
||||
*/
|
||||
meta: InframonitoringtypesStatefulSetRecordDTOMeta;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
@@ -6818,6 +6944,29 @@ export interface LlmpricingruletypesGettablePricingRulesDTO {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface LlmpricingruletypesUnmappedModelDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
modelName: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
provider?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
spanCount: number;
|
||||
}
|
||||
|
||||
export interface LlmpricingruletypesGettableUnmappedModelsDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
items: LlmpricingruletypesUnmappedModelDTO[] | null;
|
||||
}
|
||||
|
||||
export interface LlmpricingruletypesUpdatableLLMPricingRuleDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
@@ -8480,6 +8629,13 @@ export interface RuletypesRuleDTO {
|
||||
export enum RuletypesThresholdKindDTO {
|
||||
basic = 'basic',
|
||||
}
|
||||
export interface ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -8549,7 +8705,11 @@ export interface ServiceaccounttypesPostableServiceAccountRoleDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
roleId: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
serviceAccountId: string;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesServiceAccountDTO {
|
||||
@@ -10168,6 +10328,14 @@ export type GetLLMPricingRule200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListUnmappedLLMModels200 = {
|
||||
data: LlmpricingruletypesGettableUnmappedModelsDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListPromotedAndIndexedPaths200 = {
|
||||
/**
|
||||
* @type array,null
|
||||
@@ -10307,6 +10475,28 @@ export type UpdateRoutePolicy200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateServiceAccountRole201 = {
|
||||
data: TypesIdentifiableDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteServiceAccountRolePathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetServiceAccountRolePathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetServiceAccountRole200 = {
|
||||
data: ServiceaccounttypesServiceAccountRoleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListServiceAccounts200 = {
|
||||
/**
|
||||
* @type array
|
||||
@@ -10390,10 +10580,10 @@ export type GetServiceAccountRoles200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateServiceAccountRolePathParameters = {
|
||||
export type CreateServiceAccountRoleDeprecatedPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type CreateServiceAccountRole201 = {
|
||||
export type CreateServiceAccountRoleDeprecated201 = {
|
||||
data: TypesIdentifiableDTO;
|
||||
/**
|
||||
* @type string
|
||||
@@ -10401,7 +10591,7 @@ export type CreateServiceAccountRole201 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteServiceAccountRolePathParameters = {
|
||||
export type DeleteServiceAccountRoleDeprecatedPathParameters = {
|
||||
id: string;
|
||||
rid: string;
|
||||
};
|
||||
@@ -11161,6 +11351,29 @@ export type GetMyOrganization200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetPublicDashboardDataV2PathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetPublicDashboardDataV2200 = {
|
||||
data: DashboardtypesGettablePublicDashboardDataV2DTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetPublicDashboardPanelQueryRangeV2PathParameters = {
|
||||
id: string;
|
||||
key: string;
|
||||
};
|
||||
export type GetPublicDashboardPanelQueryRangeV2200 = {
|
||||
data: Querybuildertypesv5QueryRangeResponseDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type Readyz200 = {
|
||||
data: FactoryResponseDTO;
|
||||
/**
|
||||
@@ -11538,6 +11751,28 @@ export type RotateSession200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateUserRole201 = {
|
||||
data: TypesIdentifiableDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteUserRolePathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetUserRolePathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetUserRole200 = {
|
||||
data: AuthtypesUserRoleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListUsers200 = {
|
||||
/**
|
||||
* @type array
|
||||
|
||||
@@ -19,12 +19,15 @@ import type {
|
||||
|
||||
import type {
|
||||
AuthtypesPostableUserDTO,
|
||||
AuthtypesPostableUserRoleDTO,
|
||||
CreateInvite201,
|
||||
CreateResetPasswordToken201,
|
||||
CreateResetPasswordTokenPathParameters,
|
||||
CreateUser201,
|
||||
CreateUserRole201,
|
||||
DeleteUserDeprecatedPathParameters,
|
||||
DeleteUserPathParameters,
|
||||
DeleteUserRolePathParameters,
|
||||
GetMyUser200,
|
||||
GetMyUserDeprecated200,
|
||||
GetResetPasswordToken200,
|
||||
@@ -37,6 +40,8 @@ import type {
|
||||
GetUserDeprecated200,
|
||||
GetUserDeprecatedPathParameters,
|
||||
GetUserPathParameters,
|
||||
GetUserRole200,
|
||||
GetUserRolePathParameters,
|
||||
GetUsersByRoleID200,
|
||||
GetUsersByRoleIDPathParameters,
|
||||
ListUsers200,
|
||||
@@ -1154,6 +1159,267 @@ export const invalidateGetUsersByRoleID = async (
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint assigns a role to a user
|
||||
* @summary Create user role
|
||||
*/
|
||||
export const createUserRole = (
|
||||
authtypesPostableUserRoleDTO?: BodyType<AuthtypesPostableUserRoleDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateUserRole201>({
|
||||
url: `/api/v2/user_roles`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: authtypesPostableUserRoleDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateUserRoleMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createUserRole>>,
|
||||
TError,
|
||||
{ data?: BodyType<AuthtypesPostableUserRoleDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createUserRole>>,
|
||||
TError,
|
||||
{ data?: BodyType<AuthtypesPostableUserRoleDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createUserRole'];
|
||||
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 createUserRole>>,
|
||||
{ data?: BodyType<AuthtypesPostableUserRoleDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createUserRole(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateUserRoleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createUserRole>>
|
||||
>;
|
||||
export type CreateUserRoleMutationBody =
|
||||
| BodyType<AuthtypesPostableUserRoleDTO>
|
||||
| undefined;
|
||||
export type CreateUserRoleMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create user role
|
||||
*/
|
||||
export const useCreateUserRole = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createUserRole>>,
|
||||
TError,
|
||||
{ data?: BodyType<AuthtypesPostableUserRoleDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createUserRole>>,
|
||||
TError,
|
||||
{ data?: BodyType<AuthtypesPostableUserRoleDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateUserRoleMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint revokes a role from a user
|
||||
* @summary Delete user role
|
||||
*/
|
||||
export const deleteUserRole = (
|
||||
{ id }: DeleteUserRolePathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v2/user_roles/${id}`,
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteUserRoleMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteUserRole>>,
|
||||
TError,
|
||||
{ pathParams: DeleteUserRolePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteUserRole>>,
|
||||
TError,
|
||||
{ pathParams: DeleteUserRolePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteUserRole'];
|
||||
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 deleteUserRole>>,
|
||||
{ pathParams: DeleteUserRolePathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteUserRole(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteUserRoleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteUserRole>>
|
||||
>;
|
||||
|
||||
export type DeleteUserRoleMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Delete user role
|
||||
*/
|
||||
export const useDeleteUserRole = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteUserRole>>,
|
||||
TError,
|
||||
{ pathParams: DeleteUserRolePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteUserRole>>,
|
||||
TError,
|
||||
{ pathParams: DeleteUserRolePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getDeleteUserRoleMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint gets an existing user role
|
||||
* @summary Get user role
|
||||
*/
|
||||
export const getUserRole = (
|
||||
{ id }: GetUserRolePathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetUserRole200>({
|
||||
url: `/api/v2/user_roles/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetUserRoleQueryKey = ({ id }: GetUserRolePathParameters) => {
|
||||
return [`/api/v2/user_roles/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetUserRoleQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getUserRole>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetUserRolePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getUserRole>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetUserRoleQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getUserRole>>> = ({
|
||||
signal,
|
||||
}) => getUserRole({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getUserRole>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetUserRoleQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getUserRole>>
|
||||
>;
|
||||
export type GetUserRoleQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get user role
|
||||
*/
|
||||
|
||||
export function useGetUserRole<
|
||||
TData = Awaited<ReturnType<typeof getUserRole>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetUserRolePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getUserRole>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetUserRoleQueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get user role
|
||||
*/
|
||||
export const invalidateGetUserRole = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetUserRolePathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetUserRoleQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint lists all users for the organization
|
||||
* @summary List users v2
|
||||
@@ -1865,6 +2131,7 @@ export const invalidateGetRolesByUserID = async (
|
||||
|
||||
/**
|
||||
* This endpoint assigns the role to the user roles by user id
|
||||
* @deprecated
|
||||
* @summary Set user roles
|
||||
*/
|
||||
export const setRoleByUserID = (
|
||||
@@ -1936,6 +2203,7 @@ export type SetRoleByUserIDMutationBody =
|
||||
export type SetRoleByUserIDMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Set user roles
|
||||
*/
|
||||
export const useSetRoleByUserID = <
|
||||
@@ -1964,6 +2232,7 @@ export const useSetRoleByUserID = <
|
||||
};
|
||||
/**
|
||||
* This endpoint removes a role from the user by user id and role id
|
||||
* @deprecated
|
||||
* @summary Remove a role from user
|
||||
*/
|
||||
export const removeUserRoleByUserIDAndRoleID = (
|
||||
@@ -2022,6 +2291,7 @@ export type RemoveUserRoleByUserIDAndRoleIDMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @summary Remove a role from user
|
||||
*/
|
||||
export const useRemoveUserRoleByUserIDAndRoleID = <
|
||||
|
||||
1
frontend/src/assets/Logos/cassandra.svg
Normal file
1
frontend/src/assets/Logos/cassandra.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#1287b1" viewBox="0 0 24 24"><title>Apache Cassandra</title><path d="M10.374 10.53a3 3 0 0 1-.428-.222l.555.143c0 .02-.01.036-.01.055l-.117.025zm-.283 1.506-.315.253.852-1.079-1.078.391c.002.017.009.033.009.05a.57.57 0 0 1-.184.42q.154.327.375.616a3.2 3.2 0 0 1 .34-.651zm.717-2.347-.652-.82a.43.43 0 0 1-.506.162c-.054.073-.083.162-.13.24l1.258.463zm-1.666.444c-.07.314-.087.637-.05.956a.57.57 0 0 1 .451.475l.946-.606c-.067-.022-.126-.06-.191-.088l-1.119-.08.64-.14a3.2 3.2 0 0 1-.668-.554zM20.1 11.648c-.164.202.833 1.022.833 1.022s-1.654-1.022-2.234-.72c-.278.144.574.811 1.175 1.242-.428-.274-.982-.571-1.175-.408-.328.277 1.565 2.549 1.565 2.549s-2.145-2.322-2.36-2.209c-.214.114.593 1.224.593 1.224s-1.06-1.16-1.35-.959c-.29.202 1.514 3.218 1.514 3.218s-1.956-3.091-2.763-2.574c1.268 2.782.795 3.18.795 3.18s-.162-2.839-1.742-2.764c-.795.038.379 2.12.379 2.12s-1.08-1.902-1.8-1.864c1.326 2.51.854 3.53.854 3.53s.219-2.143-1.58-3.336c.682.606-.427 3.336-.427 3.336s.976-4.023-.719-3.256c-.268.121-.019 2.007-.019 2.007s-.34-2.158-.851-2.045c-.298.066-1.893 2.99-1.893 2.99s1.306-3.16.908-3.027c-.29.096-.833 1.4-.833 1.4s.265-1.287 0-1.363c-.264-.075-1.74 1.363-1.74 1.363s1.097-1.287.908-1.552c-.287-.402-.623-.42-1.022-.265-.581.226-1.363 1.287-1.363 1.287s.78-1.074.643-1.476c-.219-.647-2.46 1.249-2.46 1.249s1.325-1.25 1.022-1.514c-.303-.265-1.947-.183-2.46-.185-1.515-.004-2.039-.36-2.498-.724 1.987.997 3.803-.151 6.094.494l.21.06c-1.3-.558-2.144-1.378-2.226-2.354-.036-.416.074-.827.297-1.222.619-.4 1.29-.773 2.06-1.095a4 4 0 0 0-.064.698c0 2.44 2.203 4.417 4.92 4.417s4.92-1.977 4.92-4.417c0-.45-.083-.881-.223-1.29 1.431.404 2.45.968 3.132 1.335.022.092.045.184.053.279.024.274-.018.547-.11.814.095-.147.198-.288.28-.445.367-.997 1.855.227 1.855.227s-1.085-.454-1.06-.24c.026.215 1.628.96 1.628.96s-1.45-.455-1.362-.114c.088.34 1.817 1.703 1.817 1.703s-1.956-1.489-2.12-1.287zm-7.268 2.65.042-.008-.06.01zM9.256 9.753c.12.13.26.234.396.343l.927-.029-1.064-.788c-.093.154-.195.303-.26.474Zm10.62 3.44c.3.215.54.373.54.373s-.24-.181-.54-.374zM7.507 8.617c-.14.229-.214.492-.215.76a3.99 3.99 0 0 0 2.358 3.64q0-.008.003-.014a3.2 3.2 0 0 1-.58-.788c-.648.099-.926-.794-.336-1.08a3.2 3.2 0 0 1 .138-1.388 3.16 3.16 0 0 1-.52-1.36q-.444.105-.848.23m1.488.82q.163-.36.402-.661a.435.435 0 0 1 .568-.557c.077-.059.166-.099.248-.15a16 16 0 0 0-1.727.284c.114.388.272.76.509 1.084m2.285 3.928c1.4 0 2.633-.723 3.344-1.816a3.4 3.4 0 0 0-1.265-.539l-.297-.023.916.9-1.197-.467.704 1.078-1.074-.832-.012.006.347 1.278-.596-1.134-.098 1.33-.401-1.326-.472 1.261.114-1.359-.015-.008-.814 1.154.286-1.067c-.34.322-.605.713-.781 1.146q.143.153.303.29.484.126 1.008.128m10.145-4.434c.971-.567 1.716-1.955 1.716-1.955s-1.893 1.955-3.205 1.665c1.186-.934 1.766-2.549 1.766-2.549s-1.506 2.325-2.448 2.423c1.086-.959 1.54-2.322 1.54-2.322s-1.237 1.817-2.196 1.944c1.287-1.161 1.338-1.893 1.338-1.893s-1.781 2.302-2.499 1.943c.858-.934 1.439-2.12 1.439-2.12s-1.489 2.019-1.893 1.69c-.277-.05.454-.958.454-.958s-.908.807-1.16.606c.454-.278 1.236-1.64 1.236-1.64S16 7.505 15.621 7.304l.731-1.483s-.73 1.483-1.715 1.23c.454-.58.63-1.112.63-1.112s-.756 1.213-1.69.885c-.22-.077.273-.635.273-.635s-.626.61-1.055.534c-.43-.076.025-.858.025-.858s-.757 1.186-.908 1.136.075-.833.075-.833-.555.908-.858.858 0-.934 0-.934-.328.984-.58.909c-.252-.076-.303-.656-.303-.656s-.068.788-.429.858c-2.725.53-5.728 1.69-9.489 5.45C3.887 10.738 5.3 7.91 11.962 7.659c5.044-.191 7.399 2.137 8.177 2.17C22.51 9.93 24 7.633 24 7.633s-1.489 1.716-2.574 1.3zm-7.74.872-.608.464v.001l.054.003a3.4 3.4 0 0 0 .554-.468m1.583-.426c0-.536-.237-.929-.594-1.217a3.2 3.2 0 0 1-.165.825.393.393 0 0 1-.328.681c-.154.233-.34.445-.549.63l.661.034-.995.237c-.025.018-.045.041-.07.058a3.2 3.2 0 0 1 1.536.691c.32-.574.504-1.235.504-1.94zM10.99 7.996a3.5 3.5 0 0 0-.785.46.43.43 0 0 1-.013.357l.885.643.023-.016-.36-1.262.627 1.12c.018-.006.04-.006.058-.011l-.02-1.251.398 1.163.477-1.15.016 1.268.012.007.713-1.005-.363 1.218.009.01 1.04-.69-.759 1.05.002.005.95-.34.041-.045a.395.395 0 0 1 .394-.632 3.4 3.4 0 0 0 .27-.784 14 14 0 0 0-2.798-.168c-.286.011-.55.033-.817.053"/></svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
1
frontend/src/assets/Logos/fluxcd.svg
Normal file
1
frontend/src/assets/Logos/fluxcd.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="48 -2.25 262.5 364"><defs><style>.cls-1{fill:#326ce5}.cls-3{fill:none}</style></defs><path d="M59.724 97.778a10.183 10.183 0 0 1 0-17.073l114.07-74.16a10.18 10.18 0 0 1 11.1 0l114.07 74.16a10.183 10.183 0 0 1 0 17.073l-114.07 74.16a10.18 10.18 0 0 1-11.1 0z" class="cls-1"/><path fill="#c1d2f7" d="M197.356 110.866h7.912a6.003 6.003 0 0 0 5.2-9.005l-25.924-44.902a6.004 6.004 0 0 0-10.399 0l-25.924 44.902a6.003 6.003 0 0 0 5.2 9.005h7.912a6.003 6.003 0 0 1 6.004 6.003v51.257l5.31 3.452a12.29 12.29 0 0 0 13.395 0l5.31-3.452v-51.257a6.003 6.003 0 0 1 6.004-6.003"/><path d="M173.793 353.271a10.1 10.1 0 0 0 3.457 1.402 119 119 0 0 0-6.623-3.46zm-6.456-161.195-11.316 7.356a111 111 0 0 0 11.316 6.6zm24.015 23.716c9.739 3.115 19.813 5.648 30.11 8.186 10.92 2.692 21.973 5.427 32.818 9.009l-35.416-23.025a216 216 0 0 1-27.512-9.41zm0 62.191v.586c0 2.57-2.688 4.652-6.003 4.652H173.34c-3.315 0-6.003-2.083-6.003-4.652v-6.605c-24.906-6.402-49.873-14.485-70.86-33.82l-14.529 9.445c22.468 22.05 50.28 28.933 79.715 36.189 27.178 6.698 55.18 13.651 78.685 33.442l14.68-9.544c-18.532-16.504-40.41-23.687-63.676-29.693M93.315 300.95l38.908 25.295c20.064 5.113 40.2 11.248 58.31 23.36l14.901-9.687c-19.752-13.672-42.55-19.313-66.419-25.197-15.287-3.767-30.835-7.616-45.7-13.771m74.022-82.254a124.3 124.3 0 0 1-21.534-12.62l-14.805 9.625a124.5 124.5 0 0 0 36.339 18.708zm51.41 16.294c-9.106-2.245-18.304-4.524-27.395-7.299v13.682q5.051 1.286 10.177 2.538c29.934 7.379 60.888 15.01 85.708 39.829.61.61 1.162 1.24 1.752 1.857l9.974-6.484a10.1 10.1 0 0 0 3.475-3.8 101 101 0 0 0-3.289-3.486c-22.585-22.585-50.669-29.508-80.402-36.837m-19.933 19.933q-3.722-.918-7.462-1.855v13.21c25.817 6.561 51.803 14.688 73.455 35.04l14.521-9.44-.11-.118c-22.586-22.585-50.67-29.509-80.404-36.837m-31.477-8.587c-16.28-5.278-32.118-12.397-46.344-24.131l-14.682 9.545c17.87 15.557 38.784 22.673 61.026 28.5z" class="cls-3"/><path d="M73.24 254.96c-.348-.348-.658-.71-1-1.06l-12.514 8.136a10 10 0 0 0-1.644 1.397c1.053 1.157 2.115 2.31 3.245 3.44 22.585 22.585 50.67 29.509 80.403 36.837 25.343 6.247 51.401 12.717 73.865 29.603l14.806-9.626c-20.865-16.42-45.53-22.509-71.453-28.898-29.934-7.379-60.888-15.01-85.708-39.83" class="cls-3"/><path d="m218.864 209.962-27.512-17.886v8.476a216 216 0 0 0 27.512 9.41m-27.512 5.83v11.9c9.091 2.774 18.289 5.053 27.395 7.298 29.733 7.329 57.817 14.252 80.402 36.837a101 101 0 0 1 3.29 3.486 10.193 10.193 0 0 0-3.476-13.277l-44.683-29.05c-10.845-3.58-21.898-6.316-32.818-9.008-10.297-2.538-20.371-5.071-30.11-8.186m-24.015-9.76a111 111 0 0 1-11.316-6.6l-10.218 6.644a124.3 124.3 0 0 0 21.534 12.62zm34.192 37.88q-5.125-1.264-10.177-2.539v11.695q3.737.938 7.462 1.855c29.733 7.328 57.818 14.252 80.403 36.837.039.039.073.079.111.118l9.66-6.28c-.589-.618-1.142-1.248-1.751-1.858-24.82-24.82-55.774-32.45-85.708-39.829m-34.192-9.503a124.5 124.5 0 0 1-36.34-18.708l-10.004 6.504c14.226 11.734 30.064 18.853 46.344 24.13zm0 25.842c-22.242-5.828-43.156-12.944-61.026-28.5l-9.834 6.392c20.987 19.336 45.954 27.42 70.86 33.821zm24.015 17.732c23.266 6.006 45.144 13.19 63.675 29.693l9.78-6.358c-21.652-20.352-47.638-28.479-73.455-35.04zM81.948 247.59l-9.707 6.311c.341.35.652.712 1 1.06 24.82 24.82 55.773 32.45 85.707 39.829 25.922 6.39 50.588 12.478 71.452 28.898l9.948-6.467c-23.505-19.791-51.507-26.744-78.685-33.442-29.436-7.256-57.247-14.138-79.715-36.189m-20.621 19.284c-1.13-1.13-2.192-2.283-3.245-3.44a10.154 10.154 0 0 0 1.644 15.68l33.589 21.837c14.865 6.155 30.413 10.004 45.7 13.771 23.869 5.884 46.667 11.525 66.419 25.197l10.16-6.605c-22.463-16.886-48.521-23.356-73.864-29.603-29.733-7.328-57.818-14.252-80.403-36.837m109.3 84.34a119 119 0 0 1 6.623 3.46 10.16 10.16 0 0 0 7.645-1.402l5.639-3.666c-18.11-12.112-38.247-18.247-58.311-23.36z" class="cls-1"/></svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
1
frontend/src/assets/Logos/planetscale.svg
Normal file
1
frontend/src/assets/Logos/planetscale.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><title>PlanetScale</title><path d="M0 12C0 5.373 5.373 0 12 0c4.873 0 9.067 2.904 10.947 7.077l-15.87 15.87a12 12 0 0 1-1.935-1.099L14.99 12H12l-8.485 8.485A11.96 11.96 0 0 1 0 12m12.004 12L24 12.004C23.998 18.628 18.628 23.998 12.004 24"/></svg>
|
||||
|
After Width: | Height: | Size: 326 B |
@@ -1,7 +1,7 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
|
||||
export const ExploreHeaderToolTip = {
|
||||
url: 'https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=new-query-builder',
|
||||
url: 'https://signoz.io/docs/querying/overview/?utm_source=product&utm_medium=new-query-builder',
|
||||
text: 'More details on how to use query builder',
|
||||
};
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
||||
Set the time interval for aggregation
|
||||
<br />
|
||||
<a
|
||||
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
|
||||
href="https://signoz.io/docs/userguide/query-builder-v5/#temporal-aggregation-within-each-time-series"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#1890ff', textDecoration: 'underline' }}
|
||||
@@ -254,7 +254,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
||||
Set the time interval for aggregation
|
||||
<br />
|
||||
<a
|
||||
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
|
||||
href="https://signoz.io/docs/userguide/query-builder-v5/#temporal-aggregation-within-each-time-series"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#1890ff', textDecoration: 'underline' }}
|
||||
|
||||
@@ -52,7 +52,7 @@ const ADD_ONS = [
|
||||
key: ADD_ONS_KEYS.GROUP_BY,
|
||||
description:
|
||||
'Break down data by attributes like service name, endpoint, status code, or region. Essential for spotting patterns and comparing performance across different segments.',
|
||||
docLink: 'https://signoz.io/docs/userguide/query-builder-v5/#grouping',
|
||||
docLink: 'https://signoz.io/docs/querying/aggregation-grouping/#grouping',
|
||||
},
|
||||
{
|
||||
icon: <ScrollText size={14} />,
|
||||
@@ -61,7 +61,7 @@ const ADD_ONS = [
|
||||
description:
|
||||
'Filter grouped results based on aggregate conditions. Show only groups meeting specific criteria, like error rates > 5% or p99 latency > 500',
|
||||
docLink:
|
||||
'https://signoz.io/docs/userguide/query-builder-v5/#conditional-filtering-with-having',
|
||||
'https://signoz.io/docs/querying/result-manipulation/#conditional-filtering-with-having',
|
||||
},
|
||||
{
|
||||
icon: <ScrollText size={14} />,
|
||||
@@ -70,7 +70,7 @@ const ADD_ONS = [
|
||||
description:
|
||||
'Sort results to surface what matters most. Quickly identify slowest operations, most frequent errors, or highest resource consumers.',
|
||||
docLink:
|
||||
'https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting',
|
||||
'https://signoz.io/docs/querying/result-manipulation/#sorting--limiting',
|
||||
},
|
||||
{
|
||||
icon: <ScrollText size={14} />,
|
||||
@@ -79,7 +79,7 @@ const ADD_ONS = [
|
||||
description:
|
||||
'Show only the top/bottom N results. Perfect for focusing on outliers, reducing noise, and improving dashboard performance.',
|
||||
docLink:
|
||||
'https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting',
|
||||
'https://signoz.io/docs/querying/result-manipulation/#how-limit-works-for-time-series',
|
||||
},
|
||||
{
|
||||
icon: <ScrollText size={14} />,
|
||||
@@ -88,7 +88,7 @@ const ADD_ONS = [
|
||||
description:
|
||||
'Customize series labels using variables like {{service.name}}-{{endpoint}}. Makes charts readable at a glance during incident investigation.',
|
||||
docLink:
|
||||
'https://signoz.io/docs/userguide/query-builder-v5/#legend-formatting',
|
||||
'https://signoz.io/docs/querying/aggregation-grouping/#legend-formatting',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -99,7 +99,7 @@ const REDUCE_TO = {
|
||||
description:
|
||||
'Apply mathematical operations like sum, average, min, max, or percentiles to reduce multiple time series into a single value.',
|
||||
docLink:
|
||||
'https://signoz.io/docs/userguide/query-builder-v5/#reduce-operations',
|
||||
'https://signoz.io/docs/userguide/query-builder-v5/#result-manipulation',
|
||||
};
|
||||
|
||||
const hasValue = (value: unknown): boolean =>
|
||||
@@ -349,7 +349,7 @@ function QueryAddOns({
|
||||
<TooltipContent
|
||||
label="Group By"
|
||||
description="Break down data by attributes like service name, endpoint, status code, or region. Essential for spotting patterns and comparing performance across different segments."
|
||||
docLink="https://signoz.io/docs/userguide/query-builder-v5/#grouping"
|
||||
docLink="https://signoz.io/docs/querying/aggregation-grouping/#grouping"
|
||||
/>
|
||||
}
|
||||
placement="top"
|
||||
@@ -385,7 +385,7 @@ function QueryAddOns({
|
||||
<TooltipContent
|
||||
label="Having"
|
||||
description="Filter grouped results based on aggregate conditions. Show only groups meeting specific criteria, like error rates > 5% or p99 latency > 500"
|
||||
docLink="https://signoz.io/docs/userguide/query-builder-v5/#conditional-filtering-with-having"
|
||||
docLink="https://signoz.io/docs/querying/result-manipulation/#conditional-filtering-with-having"
|
||||
/>
|
||||
}
|
||||
placement="top"
|
||||
@@ -434,7 +434,7 @@ function QueryAddOns({
|
||||
<TooltipContent
|
||||
label="Order By"
|
||||
description="Sort results to surface what matters most. Quickly identify slowest operations, most frequent errors, or highest resource consumers."
|
||||
docLink="https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting"
|
||||
docLink="https://signoz.io/docs/querying/result-manipulation/#sorting--limiting"
|
||||
/>
|
||||
}
|
||||
placement="top"
|
||||
@@ -473,7 +473,7 @@ function QueryAddOns({
|
||||
<TooltipContent
|
||||
label="Reduce to"
|
||||
description="Apply mathematical operations like sum, average, min, max, or percentiles to reduce multiple time series into a single value."
|
||||
docLink="https://signoz.io/docs/userguide/query-builder-v5/#reduce-operations"
|
||||
docLink="https://signoz.io/docs/userguide/query-builder-v5/#result-manipulation"
|
||||
/>
|
||||
}
|
||||
placement="top"
|
||||
|
||||
@@ -65,7 +65,7 @@ function QueryAggregationOptions({
|
||||
Set the time interval for aggregation
|
||||
<br />
|
||||
<a
|
||||
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
|
||||
href="https://signoz.io/docs/userguide/query-builder-v5/#temporal-aggregation-within-each-time-series"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#1890ff', textDecoration: 'underline' }}
|
||||
|
||||
@@ -676,7 +676,7 @@ function QueryAggregationSelect({
|
||||
</span>
|
||||
<br />
|
||||
<a
|
||||
href="https://signoz.io/docs/userguide/query-builder-v5/#core-aggregation-functions"
|
||||
href="https://signoz.io/docs/querying/aggregation-grouping/#core-aggregation-functions-logs--traces"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#1890ff', textDecoration: 'underline' }}
|
||||
|
||||
@@ -44,7 +44,7 @@ function TraceOperatorSection({
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
Add Trace Matching
|
||||
<Typography.Link
|
||||
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-trace-operators"
|
||||
href="https://signoz.io/docs/querying/multi-query-analysis/#trace-matching"
|
||||
target="_blank"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
>
|
||||
@@ -106,7 +106,7 @@ export default function QueryFooter({
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
Add New Formula
|
||||
<Typography.Link
|
||||
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
|
||||
href="https://signoz.io/docs/querying/multi-query-analysis/#advanced-comparisons"
|
||||
target="_blank"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Button } from 'antd';
|
||||
import { Checkbox } from '@signozhq/ui/checkbox';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
|
||||
interface CheckboxValueRowProps {
|
||||
value: string;
|
||||
@@ -46,9 +47,11 @@ function CheckboxValueRow({
|
||||
{customRendererForValue ? (
|
||||
customRendererForValue(value)
|
||||
) : (
|
||||
<Typography.Text className="value-string" truncate={1}>
|
||||
{String(value)}
|
||||
</Typography.Text>
|
||||
<TooltipSimple title={String(value)} side="top" align="start">
|
||||
<Typography.Text className="value-string" truncate={1}>
|
||||
{String(value)}
|
||||
</Typography.Text>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
<Button type="text" className="only-btn">
|
||||
{onlyButtonLabel}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ComponentType } from 'react';
|
||||
import { TabsProps } from 'antd';
|
||||
import { History } from 'history';
|
||||
|
||||
export type TabRoutes = {
|
||||
name: React.ReactNode;
|
||||
route: string;
|
||||
Component: () => JSX.Element;
|
||||
Component: ComponentType;
|
||||
key: string;
|
||||
};
|
||||
|
||||
|
||||
34
frontend/src/components/TagBadge/TagBadge.tsx
Normal file
34
frontend/src/components/TagBadge/TagBadge.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { type MouseEvent, type ReactNode } from 'react';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
|
||||
interface TagBadgeProps {
|
||||
children: ReactNode;
|
||||
// Show a remove button (editable contexts: create modal, settings drawer).
|
||||
closable?: boolean;
|
||||
onClose?: (event: MouseEvent<HTMLButtonElement>) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// The single sienna tag chip used everywhere dashboards render tags — list rows,
|
||||
// the details header, and the tag editors. Kept as one component so the tag
|
||||
// styling stays identical across all of them.
|
||||
function TagBadge({
|
||||
children,
|
||||
closable,
|
||||
onClose,
|
||||
className,
|
||||
}: TagBadgeProps): JSX.Element {
|
||||
return (
|
||||
<Badge
|
||||
color="sienna"
|
||||
variant="outline"
|
||||
className={className}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
>
|
||||
{children}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export default TagBadge;
|
||||
@@ -15,59 +15,28 @@
|
||||
border: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
// Sienna chip — matches the dashboard list-row tag badge.
|
||||
// Sienna chip via the @signozhq Badge; this only constrains its width.
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 240px;
|
||||
height: 24px;
|
||||
padding: 2px 4px 2px 8px;
|
||||
border-radius: 50px;
|
||||
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
|
||||
color: var(--bg-sienna-400);
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 20px;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
// The tag label is a bare, chrome-less button inside the Badge (double-click to
|
||||
// edit); strip its Button styling and let it ellipsize.
|
||||
.tagLabel {
|
||||
--button-height: auto;
|
||||
--button-padding: 0;
|
||||
--button-gap: 0;
|
||||
--button-variant-ghost-background-color: transparent;
|
||||
--button-variant-ghost-hover-background-color: transparent;
|
||||
--button-variant-ghost-color: inherit;
|
||||
--button-variant-ghost-hover-color: inherit;
|
||||
--button-variant-ghost-color: currentColor;
|
||||
--button-variant-ghost-hover-color: currentColor;
|
||||
overflow: hidden;
|
||||
max-width: 200px;
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.remove {
|
||||
// Size overrides to fit the chip, plus a sienna-tinted hover — the Button's
|
||||
// default ghost hover is a grey that clashes with the chip. Resting color is
|
||||
// left at the Button default.
|
||||
--button-height: 16px;
|
||||
--button-padding: 0;
|
||||
--button-border-radius: 50%;
|
||||
--button-variant-ghost-hover-background-color: color-mix(
|
||||
in srgb,
|
||||
var(--bg-sienna-500) 22%,
|
||||
transparent
|
||||
);
|
||||
--button-variant-ghost-hover-color: var(--bg-sienna-400);
|
||||
width: 16px;
|
||||
min-width: 16px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
|
||||
@@ -2,9 +2,9 @@ import { type ChangeEvent, type KeyboardEvent, useState } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { X } from '@signozhq/icons';
|
||||
import cx from 'classnames';
|
||||
|
||||
import TagBadge from '../TagBadge/TagBadge';
|
||||
import { parseKeyValueTag } from './utils';
|
||||
|
||||
import styles from './TagKeyValueInput.module.scss';
|
||||
@@ -120,27 +120,23 @@ function TagKeyValueInput({
|
||||
onBlur={commitEdit}
|
||||
/>
|
||||
) : (
|
||||
<div key={tag} className={styles.tag} data-testid={`${testId}-chip`}>
|
||||
<TagBadge
|
||||
key={tag}
|
||||
className={styles.tag}
|
||||
closable
|
||||
onClose={(): void => removeTag(tag)}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className={styles.tagLabel}
|
||||
title="Double-click to edit"
|
||||
testId={`${testId}-chip`}
|
||||
onDoubleClick={(): void => startEdit(index)}
|
||||
>
|
||||
{tag}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={styles.remove}
|
||||
aria-label={`Remove ${tag}`}
|
||||
onClick={(): void => removeTag(tag)}
|
||||
>
|
||||
<X size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
</TagBadge>
|
||||
),
|
||||
)}
|
||||
<Input
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from 'container/AIAssistant/store/useAIAssistantStore';
|
||||
import { useThemeMode } from 'hooks/useDarkMode';
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import { IS_DEV } from 'lib/env';
|
||||
import history from 'lib/history';
|
||||
import { ROLES as UserRole } from 'types/roles';
|
||||
|
||||
@@ -30,6 +31,33 @@ import { useCmdK } from '../../providers/cmdKProvider';
|
||||
|
||||
import './cmdKPalette.scss';
|
||||
|
||||
const AuthZDevModal = IS_DEV
|
||||
? React.lazy(() =>
|
||||
import('lib/authz/devtools/AuthZDevModal/AuthZDevModal').then((m) => ({
|
||||
default: m.AuthZDevModal,
|
||||
})),
|
||||
)
|
||||
: null;
|
||||
|
||||
const AuthZDevFloatingIndicator = IS_DEV
|
||||
? React.lazy(() =>
|
||||
import('lib/authz/devtools/AuthZDevFloatingIndicator/AuthZDevFloatingIndicator').then(
|
||||
(m) => ({
|
||||
default: m.AuthZDevFloatingIndicator,
|
||||
}),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
|
||||
const openAuthZDevModal = IS_DEV
|
||||
? (): void => {
|
||||
void import('lib/authz/devtools/useAuthZDevStore').then((m) => {
|
||||
m.openAuthZDevModal();
|
||||
return m;
|
||||
});
|
||||
}
|
||||
: undefined;
|
||||
|
||||
type CmdAction = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -110,6 +138,7 @@ export function CmdKPalette({
|
||||
aiAssistant: isAIAssistantEnabled
|
||||
? { open: handleOpenAIAssistant }
|
||||
: undefined,
|
||||
authzDevTools: openAuthZDevModal ? { open: openAuthZDevModal } : undefined,
|
||||
});
|
||||
|
||||
// RBAC filter: show action if no roles set OR current user role is included
|
||||
@@ -146,37 +175,57 @@ export function CmdKPalette({
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandDialog open={open} onOpenChange={setOpen} position="top" offset={110}>
|
||||
<CommandInput placeholder="Search…" className="cmdk-input-wrapper" />
|
||||
<CommandList className="cmdk-list-scroll">
|
||||
<CommandEmpty>No results</CommandEmpty>
|
||||
{grouped.map(([section, items]) => (
|
||||
<CommandGroup
|
||||
key={section}
|
||||
heading={section}
|
||||
className="cmdk-section-heading"
|
||||
>
|
||||
{items.map((it) => (
|
||||
<CommandItem
|
||||
key={it.id}
|
||||
onSelect={(): void => handleInvoke(it)}
|
||||
value={it.name}
|
||||
className={theme === 'light' ? 'cmdk-item-light' : 'cmdk-item'}
|
||||
>
|
||||
<span
|
||||
className={cx('cmd-item-icon', it.id === 'ai-assistant' && 'noz-icon')}
|
||||
<>
|
||||
<CommandDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
position="top"
|
||||
offset={110}
|
||||
>
|
||||
<CommandInput placeholder="Search…" className="cmdk-input-wrapper" />
|
||||
<CommandList className="cmdk-list-scroll">
|
||||
<CommandEmpty>No results</CommandEmpty>
|
||||
{grouped.map(([section, items]) => (
|
||||
<CommandGroup
|
||||
key={section}
|
||||
heading={section}
|
||||
className="cmdk-section-heading"
|
||||
>
|
||||
{items.map((it) => (
|
||||
<CommandItem
|
||||
key={it.id}
|
||||
onSelect={(): void => handleInvoke(it)}
|
||||
value={it.name}
|
||||
className={theme === 'light' ? 'cmdk-item-light' : 'cmdk-item'}
|
||||
>
|
||||
{it.icon}
|
||||
</span>
|
||||
{it.name}
|
||||
{it.shortcut && it.shortcut.length > 0 && (
|
||||
<CommandShortcut>{it.shortcut.join(' • ')}</CommandShortcut>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
<span
|
||||
className={cx(
|
||||
'cmd-item-icon',
|
||||
it.id === 'ai-assistant' && 'noz-icon',
|
||||
)}
|
||||
>
|
||||
{it.icon}
|
||||
</span>
|
||||
{it.name}
|
||||
{it.shortcut && it.shortcut.length > 0 && (
|
||||
<CommandShortcut>{it.shortcut.join(' • ')}</CommandShortcut>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
{IS_DEV && AuthZDevModal && (
|
||||
<React.Suspense fallback={null}>
|
||||
<AuthZDevModal />
|
||||
</React.Suspense>
|
||||
)}
|
||||
{IS_DEV && AuthZDevFloatingIndicator && (
|
||||
<React.Suspense fallback={null}>
|
||||
<AuthZDevFloatingIndicator />
|
||||
</React.Suspense>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const apDexToolTipText =
|
||||
"Apdex is a way to measure your users' satisfaction with the response time of your web service. It's represented as a score from 0-1.";
|
||||
export const apDexToolTipUrl =
|
||||
'https://signoz.io/docs/userguide/metrics/#apdex?utm_source=product&utm_medium=frontend&utm_campaign=apdex';
|
||||
'https://signoz.io/docs/alerts-management/apdex-alerts/?utm_source=product&utm_medium=frontend&utm_campaign=apdex';
|
||||
export const apDexToolTipUrlText = 'Learn more about Apdex.';
|
||||
|
||||
@@ -31,6 +31,12 @@ export enum LOCALSTORAGE {
|
||||
METRICS_LIST_OPTIONS = 'METRICS_LIST_OPTIONS',
|
||||
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
|
||||
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
|
||||
QUICK_FILTERS_WIDTH_LOGS = 'QUICK_FILTERS_WIDTH_LOGS',
|
||||
QUICK_FILTERS_WIDTH_TRACES = 'QUICK_FILTERS_WIDTH_TRACES',
|
||||
QUICK_FILTERS_WIDTH_METER = 'QUICK_FILTERS_WIDTH_METER',
|
||||
QUICK_FILTERS_WIDTH_API_MONITORING = 'QUICK_FILTERS_WIDTH_API_MONITORING',
|
||||
QUICK_FILTERS_WIDTH_EXCEPTIONS = 'QUICK_FILTERS_WIDTH_EXCEPTIONS',
|
||||
QUICK_FILTERS_WIDTH_INFRA = 'QUICK_FILTERS_WIDTH_INFRA',
|
||||
FUNNEL_STEPS = 'FUNNEL_STEPS',
|
||||
SPAN_DETAILS_PINNED_ATTRIBUTES = 'SPAN_DETAILS_PINNED_ATTRIBUTES',
|
||||
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',
|
||||
|
||||
@@ -90,7 +90,8 @@ const ROUTES = {
|
||||
AI_ASSISTANT_ICON_PREVIEW: '/ai-assistant-icon-preview',
|
||||
MCP_SERVER: '/settings/mcp-server',
|
||||
LLM_OBSERVABILITY_BASE: '/llm-observability',
|
||||
LLM_OBSERVABILITY_MODEL_PRICING: '/llm-observability/settings/model-pricing',
|
||||
LLM_OBSERVABILITY_OVERVIEW: '/llm-observability/overview',
|
||||
LLM_OBSERVABILITY_CONFIGURATION: '/llm-observability/configuration',
|
||||
} as const;
|
||||
|
||||
export default ROUTES;
|
||||
|
||||
@@ -43,10 +43,17 @@ type ActionDeps = {
|
||||
aiAssistant?: {
|
||||
open: () => void;
|
||||
};
|
||||
/**
|
||||
* Provided only in development mode. Opens the AuthZ DevTools modal
|
||||
* for testing permission overrides.
|
||||
*/
|
||||
authzDevTools?: {
|
||||
open: () => void;
|
||||
};
|
||||
};
|
||||
|
||||
export function createShortcutActions(deps: ActionDeps): CmdAction[] {
|
||||
const { navigate, handleThemeChange, aiAssistant } = deps;
|
||||
const { navigate, handleThemeChange, aiAssistant, authzDevTools } = deps;
|
||||
|
||||
const actions: CmdAction[] = [
|
||||
{
|
||||
@@ -302,5 +309,17 @@ export function createShortcutActions(deps: ActionDeps): CmdAction[] {
|
||||
});
|
||||
}
|
||||
|
||||
if (authzDevTools) {
|
||||
actions.push({
|
||||
id: 'authz-devtools',
|
||||
name: 'AuthZ DevTools',
|
||||
keywords: 'authz permissions rbac debug devtools override testing',
|
||||
section: 'Dev',
|
||||
icon: <Settings size={14} />,
|
||||
roles: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
perform: authzDevTools.open,
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
@@ -177,7 +177,13 @@ export default function ConversationView({
|
||||
conversationId={conversationId}
|
||||
messages={messages}
|
||||
isStreaming={isStreamingHere}
|
||||
onSendSuggestedPrompt={(text): void => handleSend(text)}
|
||||
onSendSuggestedPrompt={(text): void => {
|
||||
handleSend(
|
||||
text,
|
||||
undefined,
|
||||
autoContexts.length > 0 ? autoContexts : undefined,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{showDisclaimer && (
|
||||
<div className={disclaimerClass} role="note" aria-live="polite">
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { MessageContext } from 'api/ai-assistant/chat';
|
||||
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
|
||||
import { VariantContext } from 'container/AIAssistant/VariantContext';
|
||||
|
||||
const CHIP_ID = 'recent-errors';
|
||||
const CHIP_TEXT = 'Show me recent errors';
|
||||
|
||||
// Auto-derived page context that a normal (typed) send would attach. The chip
|
||||
// send must forward the exact same array.
|
||||
const mockAutoContexts: MessageContext[] = [
|
||||
{
|
||||
source: 'auto',
|
||||
type: 'dashboard',
|
||||
resourceId: 'dashboard-123',
|
||||
resourceName: 'Checkout dashboard',
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('api/common/logEvent', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('container/AIAssistant/getAutoContexts', () => ({
|
||||
getAutoContexts: jest.fn(() => mockAutoContexts),
|
||||
}));
|
||||
|
||||
jest.mock('container/AIAssistant/hooks/useAIAssistantAnalyticsContext', () => ({
|
||||
normalizePage: (page: string): string => page,
|
||||
useAIAssistantAnalyticsContext: (): unknown => ({ threadId: 'thread-1' }),
|
||||
}));
|
||||
|
||||
// ChatInput is heavy and irrelevant here — the chip path lives entirely in the
|
||||
// empty state. Provide a lightweight stub plus the `autoContextKey` named export
|
||||
// ConversationView imports for its dismissed-context filter.
|
||||
jest.mock('container/AIAssistant/components/ChatInput', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="chat-input" />,
|
||||
autoContextKey: (): string => '',
|
||||
}));
|
||||
|
||||
jest.mock('container/AIAssistant/components/ConversationSkeleton', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="skeleton" />,
|
||||
}));
|
||||
|
||||
// VirtualizedMessages renders the real empty-state chips; stub only its
|
||||
// never-rendered-in-empty-state children and the virtual list.
|
||||
jest.mock('components/Noz/Noz', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="noz" />,
|
||||
}));
|
||||
|
||||
jest.mock('container/AIAssistant/components/MessageBubble', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
jest.mock('container/AIAssistant/components/StreamingMessage', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
jest.mock('react-virtuoso', () => ({
|
||||
__esModule: true,
|
||||
Virtuoso: (): null => null,
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/AIAssistant/components/VirtualizedMessages/useEmptyStateChips',
|
||||
() => ({
|
||||
useEmptyStateChips: (): { chips: { id: string; text: string }[] } => ({
|
||||
chips: [{ id: CHIP_ID, text: CHIP_TEXT }],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line import/first
|
||||
import ConversationView from '../ConversationView';
|
||||
|
||||
const CONVERSATION_ID = 'conv-1';
|
||||
|
||||
function renderView(variant: 'panel' | 'page' | 'modal'): {
|
||||
getByTestId: (id: string) => HTMLElement;
|
||||
} {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={['/dashboard/dashboard-123']}>
|
||||
<VariantContext.Provider value={variant}>
|
||||
<ConversationView conversationId={CONVERSATION_ID} />
|
||||
</VariantContext.Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('ConversationView — empty-state chip context', () => {
|
||||
let sendMessage: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
sendMessage = jest.fn();
|
||||
useAIAssistantStore.setState({
|
||||
conversations: {
|
||||
[CONVERSATION_ID]: {
|
||||
id: CONVERSATION_ID,
|
||||
messages: [],
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
},
|
||||
},
|
||||
streams: {},
|
||||
activeConversationId: CONVERSATION_ID,
|
||||
isLoadingThread: false,
|
||||
sendMessage,
|
||||
} as unknown as Partial<ReturnType<typeof useAIAssistantStore.getState>>);
|
||||
});
|
||||
|
||||
it('forwards the page auto-contexts when a chip is clicked (embedded variant)', () => {
|
||||
const { getByTestId } = renderView('panel');
|
||||
|
||||
fireEvent.click(getByTestId(`empty-state-chip-${CHIP_ID}`));
|
||||
|
||||
// The chip send must carry the same auto-contexts a typed message would.
|
||||
expect(sendMessage).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessage).toHaveBeenCalledWith(
|
||||
CHIP_TEXT,
|
||||
undefined,
|
||||
mockAutoContexts,
|
||||
);
|
||||
});
|
||||
|
||||
it('sends undefined contexts on the standalone page (no page context to attach)', () => {
|
||||
const { getByTestId } = renderView('page');
|
||||
|
||||
fireEvent.click(getByTestId(`empty-state-chip-${CHIP_ID}`));
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessage).toHaveBeenCalledWith(CHIP_TEXT, undefined, undefined);
|
||||
});
|
||||
});
|
||||
@@ -68,7 +68,7 @@ function AlertChannels(): JSX.Element {
|
||||
<RightActionContainer>
|
||||
<TextToolTip
|
||||
text={t('tooltip_notification_channels')}
|
||||
url="https://signoz.io/docs/userguide/alerts-management/#setting-notification-channel"
|
||||
url="https://signoz.io/docs/setup-alerts-notification/"
|
||||
/>
|
||||
|
||||
<Tooltip
|
||||
|
||||
@@ -163,12 +163,23 @@
|
||||
}
|
||||
|
||||
&.filter-visible {
|
||||
// Width is owned by ResizableBox (inline style); this section is the
|
||||
// ResizableBox root, so it stays position: relative for the drag handle.
|
||||
.api-quick-filter-left-section {
|
||||
width: 260px;
|
||||
.resizable-box__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.quick-filters-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.api-module-right-section {
|
||||
width: calc(100% - 260px);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,49 @@ import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ResizableBox } from 'periscope/components/ResizableBox';
|
||||
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
|
||||
|
||||
import DomainList from './Domains/DomainList';
|
||||
|
||||
import './Explorer.styles.scss';
|
||||
|
||||
const QUICK_FILTERS_DEFAULT_WIDTH = 260;
|
||||
const QUICK_FILTERS_MIN_WIDTH = 240;
|
||||
const QUICK_FILTERS_MAX_WIDTH = 500;
|
||||
|
||||
function Explorer(): JSX.Element {
|
||||
useEffect(() => {
|
||||
logEvent('API Monitoring: Landing page visited', {});
|
||||
}, []);
|
||||
|
||||
const {
|
||||
initialWidth: quickFiltersInitialWidth,
|
||||
persistWidth: persistQuickFiltersWidth,
|
||||
} = usePanelWidth({
|
||||
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_API_MONITORING,
|
||||
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
|
||||
minWidth: QUICK_FILTERS_MIN_WIDTH,
|
||||
maxWidth: QUICK_FILTERS_MAX_WIDTH,
|
||||
});
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className={cx('api-monitoring-page', 'filter-visible')}>
|
||||
<section className="api-quick-filter-left-section">
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
|
||||
initialWidth={quickFiltersInitialWidth}
|
||||
minWidth={QUICK_FILTERS_MIN_WIDTH}
|
||||
maxWidth={QUICK_FILTERS_MAX_WIDTH}
|
||||
onResize={persistQuickFiltersWidth}
|
||||
resetToDefaultOnDoubleClick
|
||||
withHandle
|
||||
className="api-quick-filter-left-section"
|
||||
handleTestId="quick-filters-resize-handle"
|
||||
>
|
||||
<QuickFilters
|
||||
className="qf-api-monitoring"
|
||||
source={QuickFiltersSource.API_MONITORING}
|
||||
@@ -27,7 +55,7 @@ function Explorer(): JSX.Element {
|
||||
showQueryName={false}
|
||||
handleFilterVisibilityChange={(): void => {}}
|
||||
/>
|
||||
</section>
|
||||
</ResizableBox>
|
||||
<DomainList />
|
||||
</div>
|
||||
</Sentry.ErrorBoundary>
|
||||
|
||||
@@ -29,8 +29,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
let url = '';
|
||||
switch (option) {
|
||||
case AlertTypes.ANOMALY_BASED_ALERT:
|
||||
url =
|
||||
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples';
|
||||
url = 'https://signoz.io/docs/alerts-management/anomaly-based-alerts/';
|
||||
break;
|
||||
case AlertTypes.METRICS_BASED_ALERT:
|
||||
url =
|
||||
|
||||
@@ -31,8 +31,7 @@ export const ALERT_TYPE_URL_MAP: Record<
|
||||
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||
},
|
||||
[AlertTypes.ANOMALY_BASED_ALERT]: {
|
||||
selection:
|
||||
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples',
|
||||
selection: 'https://signoz.io/docs/alerts-management/anomaly-based-alerts/',
|
||||
creation:
|
||||
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||
},
|
||||
|
||||
@@ -149,6 +149,18 @@
|
||||
line-height: 22px; /* 157.143% */
|
||||
letter-spacing: -0.07px;
|
||||
padding: 20px 16px 0px 16px;
|
||||
/* Preserve author-entered line breaks in the description. */
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
a {
|
||||
color: var(--accent-primary);
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-variables {
|
||||
|
||||
@@ -43,6 +43,7 @@ import { sortLayout } from 'providers/Dashboard/util';
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { linkifyText } from 'utils/linkifyText';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -515,7 +516,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
</div>
|
||||
)}
|
||||
{!isEmpty(description) && (
|
||||
<section className="dashboard-description-section">{description}</section>
|
||||
<section className="dashboard-description-section">
|
||||
{linkifyText(description ?? '')}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{!isEmpty(dashboardVariables) && (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { MouseEvent as ReactMouseEvent } from 'react';
|
||||
import type { PrecisionOption } from 'components/Graph/types';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
|
||||
@@ -27,7 +28,7 @@ interface PieArcProps {
|
||||
fill: string;
|
||||
onEnter: (slice: PieSlice, centroidX: number, centroidY: number) => void;
|
||||
onLeave: () => void;
|
||||
onClick?: (slice: PieSlice) => void;
|
||||
onClick?: (slice: PieSlice, event: ReactMouseEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +73,7 @@ export default function PieArc({
|
||||
<g
|
||||
onMouseEnter={(): void => onEnter(slice, centroidX, centroidY)}
|
||||
onMouseLeave={onLeave}
|
||||
onClick={(): void => onClick?.(slice)}
|
||||
onClick={(event): void => onClick?.(slice, event)}
|
||||
>
|
||||
<path d={arcPath} fill={fill} />
|
||||
{shouldShowLabel && (
|
||||
|
||||
@@ -80,6 +80,7 @@ describe('PieArc', () => {
|
||||
expect(onLeave).toHaveBeenCalledTimes(1);
|
||||
|
||||
fireEvent.click(g);
|
||||
expect(onClick).toHaveBeenCalledWith(SLICE);
|
||||
// onClick now also receives the DOM event (for drill-down popover positioning).
|
||||
expect(onClick).toHaveBeenCalledWith(SLICE, expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { MouseEvent as ReactMouseEvent } from 'react';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PrecisionOption } from 'components/Graph/types';
|
||||
import {
|
||||
@@ -79,6 +80,10 @@ export interface PieSlice {
|
||||
label: string;
|
||||
value: number;
|
||||
color: string;
|
||||
/** Source query of the slice's value column — the drill-down target (present for V2 panels). */
|
||||
queryName?: string;
|
||||
/** Group-by key→value of the slice's source row, used to build drill-down filters. */
|
||||
labels?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,7 +104,7 @@ export interface PieChartProps {
|
||||
* (shared GRAPH_VISIBILITY_STATES, keyed by label). Omit to disable persistence.
|
||||
*/
|
||||
id?: string;
|
||||
/** Fired when a slice (or its legend entry) is clicked. */
|
||||
onSliceClick?: (slice: PieSlice) => void;
|
||||
/** Fired when a slice's arc is clicked; carries the DOM event for popover positioning. */
|
||||
onSliceClick?: (slice: PieSlice, event: ReactMouseEvent) => void;
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Input } from '@signozhq/ui/input';
|
||||
import { Button } from 'antd';
|
||||
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
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';
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
|
||||
import { getChartManagerColumns } from './getChartMangerColumns';
|
||||
import { ExtendedChartDataset, getDefaultTableDataSet } from './utils';
|
||||
@@ -44,7 +44,6 @@ export default function ChartManager({
|
||||
decimalPrecision = PrecisionOptionsEnum.TWO,
|
||||
onCancel,
|
||||
}: ChartManagerProps): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
const { legendItemsMap } = useLegendsSync({
|
||||
config,
|
||||
subscribeToFocusChange: false,
|
||||
@@ -136,11 +135,9 @@ export default function ChartManager({
|
||||
|
||||
const handleSave = useCallback((): void => {
|
||||
syncSeriesVisibilityToLocalStorage();
|
||||
notifications.success({
|
||||
message: 'The updated graphs & legends are saved',
|
||||
});
|
||||
toast.success('The updated graphs & legends are saved');
|
||||
onCancel?.();
|
||||
}, [syncSeriesVisibilityToLocalStorage, notifications, onCancel]);
|
||||
}, [syncSeriesVisibilityToLocalStorage, onCancel]);
|
||||
|
||||
return (
|
||||
<div className="chart-manager-container">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { render, screen } from 'tests/test-utils';
|
||||
import ChartManager from '../ChartManager';
|
||||
|
||||
const mockSyncSeriesVisibilityToLocalStorage = jest.fn();
|
||||
const mockNotificationsSuccess = jest.fn();
|
||||
const mockToastSuccess = jest.fn();
|
||||
|
||||
jest.mock('lib/uPlotV2/context/PlotContext', () => ({
|
||||
usePlotContext: (): {
|
||||
@@ -46,12 +46,11 @@ jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
}): boolean => s.dashboardData?.locked ?? false,
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
useNotifications: (): { notifications: { success: jest.Mock } } => ({
|
||||
notifications: {
|
||||
success: mockNotificationsSuccess,
|
||||
},
|
||||
}),
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: {
|
||||
success: (...args: unknown[]): unknown => mockToastSuccess(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('components/ResizeTable', () => {
|
||||
@@ -160,7 +159,7 @@ describe('ChartManager', () => {
|
||||
expect(screen.queryByTestId('row-2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls syncSeriesVisibilityToLocalStorage, notifications.success, and onCancel when Save is clicked', async () => {
|
||||
it('calls syncSeriesVisibilityToLocalStorage, toast.success, and onCancel when Save is clicked', async () => {
|
||||
render(
|
||||
<ChartManager
|
||||
config={createMockConfig() as UPlotConfigBuilder}
|
||||
@@ -172,9 +171,9 @@ describe('ChartManager', () => {
|
||||
await userEvent.click(screen.getByRole('button', { name: /Save/ }));
|
||||
|
||||
expect(mockSyncSeriesVisibilityToLocalStorage).toHaveBeenCalledTimes(1);
|
||||
expect(mockNotificationsSuccess).toHaveBeenCalledWith({
|
||||
message: 'The updated graphs & legends are saved',
|
||||
});
|
||||
expect(mockToastSuccess).toHaveBeenCalledWith(
|
||||
'The updated graphs & legends are saved',
|
||||
);
|
||||
expect(mockOnCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,9 +2,17 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: auto;
|
||||
flex-direction: column;
|
||||
|
||||
// Stacked children (the FullView / standalone graph-manager) sit below the chart
|
||||
// in the same container; size the chart region to its content so they aren't
|
||||
// pushed out. Only this case opts out of filling the height — the dashboard grid,
|
||||
// alert preview, and other charts keep 100% so they fill their container.
|
||||
&--with-layout-children {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&--legend-right {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ export default function ChartLayout({
|
||||
className={cx('chart-layout', {
|
||||
'chart-layout--legend-right':
|
||||
legendConfig.position === LegendPosition.RIGHT,
|
||||
'chart-layout--with-layout-children': !!layoutChildren,
|
||||
})}
|
||||
>
|
||||
<div className="chart-layout__content">
|
||||
|
||||
@@ -717,13 +717,13 @@ function ExplorerOptions({
|
||||
|
||||
const infoIconLink = useMemo(() => {
|
||||
if (isLogsExplorer) {
|
||||
return 'https://signoz.io/docs/product-features/logs-explorer/?utm_source=product&utm_medium=logs-explorer-toolbar';
|
||||
return 'https://signoz.io/docs/userguide/logs_query_builder/?utm_source=product&utm_medium=logs-explorer-toolbar';
|
||||
}
|
||||
// TODO: Add metrics explorer info icon link
|
||||
if (isMetricsExplorer) {
|
||||
return '';
|
||||
}
|
||||
return 'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=trace-explorer-toolbar';
|
||||
return 'https://signoz.io/docs/userguide/traces/?utm_source=product&utm_medium=trace-explorer-toolbar';
|
||||
}, [isLogsExplorer, isMetricsExplorer]);
|
||||
|
||||
const getQueryName = (query: Query): string => {
|
||||
|
||||
@@ -201,7 +201,7 @@ export default function SavedViews({
|
||||
});
|
||||
|
||||
window.open(
|
||||
'https://signoz.io/docs/product-features/saved-view/',
|
||||
'https://signoz.io/docs/metrics-management/metrics-explorer/#saved-views-in-metrics-explorer',
|
||||
'_blank',
|
||||
'noopener noreferrer',
|
||||
);
|
||||
|
||||
@@ -29,12 +29,12 @@ export const checkListStepToPreferenceKeyMap = {
|
||||
|
||||
export const DOCS_LINKS = {
|
||||
ADD_DATA_SOURCE: 'https://signoz.io/docs/instrumentation/overview/',
|
||||
SEND_LOGS: 'https://signoz.io/docs/userguide/logs/',
|
||||
SEND_LOGS: 'https://signoz.io/docs/userguide/logs_query_builder/',
|
||||
SEND_TRACES: 'https://signoz.io/docs/userguide/traces/',
|
||||
SEND_METRICS: 'https://signoz.io/docs/metrics-management/metrics-explorer/',
|
||||
SETUP_ALERTS: 'https://signoz.io/docs/userguide/alerts-management/',
|
||||
SETUP_ALERTS: 'https://signoz.io/docs/alerts/',
|
||||
SETUP_SAVED_VIEWS:
|
||||
'https://signoz.io/docs/product-features/saved-view/#step-2-save-your-view',
|
||||
'https://signoz.io/docs/metrics-management/metrics-explorer/#saved-views-in-metrics-explorer',
|
||||
SETUP_DASHBOARDS: 'https://signoz.io/docs/userguide/manage-dashboards/',
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import K8sBaseDetails from 'container/InfraMonitoringK8s/Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from 'container/InfraMonitoringK8s/Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from 'container/InfraMonitoringK8s/Base/types';
|
||||
@@ -16,6 +17,8 @@ import {
|
||||
} from 'container/InfraMonitoringK8s/hooks';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { ResizableBox } from 'periscope/components/ResizableBox';
|
||||
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -40,8 +43,21 @@ import { getHostsQuickFiltersConfig } from './utils';
|
||||
import styles from './InfraMonitoringHosts.module.scss';
|
||||
import { ArrowUpToLine, Filter } from '@signozhq/icons';
|
||||
|
||||
const QUICK_FILTERS_DEFAULT_WIDTH = 280;
|
||||
const QUICK_FILTERS_MIN_WIDTH = 240;
|
||||
const QUICK_FILTERS_MAX_WIDTH = 500;
|
||||
|
||||
function Hosts(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
const {
|
||||
initialWidth: quickFiltersInitialWidth,
|
||||
persistWidth: persistQuickFiltersWidth,
|
||||
} = usePanelWidth({
|
||||
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_INFRA,
|
||||
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
|
||||
minWidth: QUICK_FILTERS_MIN_WIDTH,
|
||||
maxWidth: QUICK_FILTERS_MAX_WIDTH,
|
||||
});
|
||||
const [, setCurrentPage] = useInfraMonitoringPageListing();
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
|
||||
|
||||
@@ -139,7 +155,18 @@ function Hosts(): JSX.Element {
|
||||
<div className={styles.infraMonitoringContainer}>
|
||||
<div className={styles.infraContentRow}>
|
||||
{showFilters && (
|
||||
<div className={styles.quickFiltersContainer}>
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
|
||||
initialWidth={quickFiltersInitialWidth}
|
||||
minWidth={QUICK_FILTERS_MIN_WIDTH}
|
||||
maxWidth={QUICK_FILTERS_MAX_WIDTH}
|
||||
onResize={persistQuickFiltersWidth}
|
||||
resetToDefaultOnDoubleClick
|
||||
withHandle
|
||||
className={styles.quickFiltersContainer}
|
||||
handleTestId="quick-filters-resize-handle"
|
||||
>
|
||||
<div className={styles.quickFiltersContainerHeader}>
|
||||
<Typography.Text>Filters</Typography.Text>
|
||||
<Tooltip title="Collapse Filters">
|
||||
@@ -156,7 +183,7 @@ function Hosts(): JSX.Element {
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleQuickFiltersChange}
|
||||
/>
|
||||
</div>
|
||||
</ResizableBox>
|
||||
)}
|
||||
<div
|
||||
className={`${styles.listContainer}${
|
||||
|
||||
@@ -44,26 +44,20 @@
|
||||
}
|
||||
|
||||
.quickFiltersContainer {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
// Width is owned by ResizableBox (inline style). No overflow here: the panel
|
||||
// must not scroll (QuickFilters scrolls its own list internally) — an
|
||||
// overflow on the root would also clip the edge-mounted resize grip.
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
height: 0.1rem;
|
||||
:global(.resizable-box__content) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
|
||||
:global(.quick-filters-container) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:global(.ant-collapse-header) {
|
||||
@@ -142,7 +136,9 @@
|
||||
}
|
||||
|
||||
.listContainerFiltersVisible {
|
||||
max-width: calc(100% - 280px);
|
||||
// Width is driven by flex now that the filters panel is resizable; the list
|
||||
// fills whatever space the (variable-width) panel leaves.
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.quickFiltersToggleContainer {
|
||||
|
||||
@@ -82,7 +82,7 @@ export function K8sEmptyState({
|
||||
<span className={styles.message}>
|
||||
Please refer to{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/userguide/hostmetrics/"
|
||||
href="https://signoz.io/docs/infrastructure-monitoring/hostmetrics/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
||||
@@ -611,7 +611,7 @@ describe('K8sBaseList', () => {
|
||||
expect(link).toBeInTheDocument();
|
||||
expect(link).toHaveAttribute(
|
||||
'href',
|
||||
'https://signoz.io/docs/userguide/hostmetrics/',
|
||||
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,40 +44,15 @@
|
||||
}
|
||||
|
||||
.quickFiltersContainer {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
// Width is owned by ResizableBox (inline style). No overflow here: the panel
|
||||
// must not scroll (QuickFilters scrolls its own list internally) — an
|
||||
// overflow on the root would also clip the edge-mounted resize grip.
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
height: 0.1rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
|
||||
}
|
||||
|
||||
:global(.ant-collapse-header) {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
:global(.ant-collapse-content-box) {
|
||||
padding: 0 !important;
|
||||
padding-block: 0 !important;
|
||||
|
||||
:global(.quick-filters .checkbox-filter) {
|
||||
padding-left: 18px;
|
||||
}
|
||||
:global(.resizable-box__content) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:global(.quick-filters) {
|
||||
@@ -111,22 +86,6 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.quickFiltersCategoryLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.quickFiltersCategoryLabelIcon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.quickFiltersCategoryLabelContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.listContainer {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -158,5 +117,118 @@
|
||||
}
|
||||
|
||||
.listContainerFiltersVisible {
|
||||
max-width: calc(100% - 280px);
|
||||
// Width is driven by flex now that the filters panel is resizable; the list
|
||||
// fills whatever space the (variable-width) panel leaves.
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.categorySelectorSection {
|
||||
padding: var(--spacing-4);
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
margin-bottom: var(--spacing-4);
|
||||
|
||||
&[data-type='filter'] {
|
||||
padding: 0 var(--spacing-4);
|
||||
}
|
||||
}
|
||||
|
||||
.sectionLabel {
|
||||
font-size: var(--periscope-font-size-small);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--l3-foreground);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sectionLine {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--l1-border);
|
||||
}
|
||||
|
||||
.categoryCard {
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: var(--spacing-3);
|
||||
padding: var(--spacing-2);
|
||||
}
|
||||
|
||||
.categoryList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-1);
|
||||
}
|
||||
|
||||
.categoryItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-5);
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
border: none;
|
||||
border-radius: var(--spacing-2);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
--typography-color: var(--l1-foreground);
|
||||
|
||||
&:hover:not(.categoryItemSelected) {
|
||||
background: var(--l2-background-hover);
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--l3-foreground);
|
||||
flex-shrink: 0;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
&.categoryItemSelected {
|
||||
background: var(--accent-primary);
|
||||
color: var(--l1-background);
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
background: var(--accent-primary-hover, var(--accent-primary));
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quickFiltersSection {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
|
||||
:global(.quick-filters-container) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Button, CollapseProps } from 'antd';
|
||||
import { Collapse, Tooltip } from 'antd';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import {
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
Workflow,
|
||||
} from '@signozhq/icons';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ResizableBox } from 'periscope/components/ResizableBox';
|
||||
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { FeatureKeys } from '../../constants/features';
|
||||
@@ -57,9 +59,23 @@ import K8sVolumesList from './Volumes/K8sVolumesList';
|
||||
|
||||
import styles from './InfraMonitoringK8s.module.scss';
|
||||
|
||||
const QUICK_FILTERS_DEFAULT_WIDTH = 280;
|
||||
const QUICK_FILTERS_MIN_WIDTH = 240;
|
||||
const QUICK_FILTERS_MAX_WIDTH = 500;
|
||||
|
||||
export default function InfraMonitoringK8s(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
|
||||
const {
|
||||
initialWidth: quickFiltersInitialWidth,
|
||||
persistWidth: persistQuickFiltersWidth,
|
||||
} = usePanelWidth({
|
||||
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_INFRA,
|
||||
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
|
||||
minWidth: QUICK_FILTERS_MIN_WIDTH,
|
||||
maxWidth: QUICK_FILTERS_MAX_WIDTH,
|
||||
});
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useInfraMonitoringCategory();
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
|
||||
const [, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
@@ -113,192 +129,74 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const renderCategoryLabel = (
|
||||
icon: JSX.Element,
|
||||
label: string,
|
||||
): JSX.Element => (
|
||||
<div className={styles.quickFiltersCategoryLabel}>
|
||||
<div className={styles.quickFiltersCategoryLabelContainer}>
|
||||
{icon}
|
||||
<Typography.Text>{label}</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
const categories = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: K8sCategories.PODS,
|
||||
label: 'Pods',
|
||||
icon: <Container size={14} />,
|
||||
config: GetPodsQuickFiltersConfig(dotMetricsEnabled),
|
||||
},
|
||||
{
|
||||
key: K8sCategories.NODES,
|
||||
label: 'Nodes',
|
||||
icon: <Workflow size={14} />,
|
||||
config: GetNodesQuickFiltersConfig(dotMetricsEnabled),
|
||||
},
|
||||
{
|
||||
key: K8sCategories.NAMESPACES,
|
||||
label: 'Namespaces',
|
||||
icon: <FilePenLine size={14} />,
|
||||
config: GetNamespaceQuickFiltersConfig(dotMetricsEnabled),
|
||||
},
|
||||
{
|
||||
key: K8sCategories.CLUSTERS,
|
||||
label: 'Clusters',
|
||||
icon: <Boxes size={14} />,
|
||||
config: GetClustersQuickFiltersConfig(dotMetricsEnabled),
|
||||
},
|
||||
{
|
||||
key: K8sCategories.DEPLOYMENTS,
|
||||
label: 'Deployments',
|
||||
icon: <Computer size={14} />,
|
||||
config: GetDeploymentsQuickFiltersConfig(dotMetricsEnabled),
|
||||
},
|
||||
{
|
||||
key: K8sCategories.JOBS,
|
||||
label: 'Jobs',
|
||||
icon: <Bolt size={14} />,
|
||||
config: GetJobsQuickFiltersConfig(dotMetricsEnabled),
|
||||
},
|
||||
{
|
||||
key: K8sCategories.DAEMONSETS,
|
||||
label: 'DaemonSets',
|
||||
icon: <Group size={14} />,
|
||||
config: GetDaemonsetsQuickFiltersConfig(dotMetricsEnabled),
|
||||
},
|
||||
{
|
||||
key: K8sCategories.STATEFULSETS,
|
||||
label: 'StatefulSets',
|
||||
icon: <ArrowUpDown size={14} />,
|
||||
config: GetStatefulsetsQuickFiltersConfig(dotMetricsEnabled),
|
||||
},
|
||||
{
|
||||
key: K8sCategories.VOLUMES,
|
||||
label: 'Volumes',
|
||||
icon: <HardDrive size={14} />,
|
||||
config: GetVolumesQuickFiltersConfig(dotMetricsEnabled),
|
||||
},
|
||||
],
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
|
||||
const items: CollapseProps['items'] = [
|
||||
{
|
||||
label: renderCategoryLabel(
|
||||
<Container size={14} className={styles.quickFiltersCategoryLabelIcon} />,
|
||||
'Pods',
|
||||
),
|
||||
key: K8sCategories.PODS,
|
||||
showArrow: false,
|
||||
children: (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={GetPodsQuickFiltersConfig(dotMetricsEnabled)}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: renderCategoryLabel(
|
||||
<Workflow size={14} className={styles.quickFiltersCategoryLabelIcon} />,
|
||||
'Nodes',
|
||||
),
|
||||
key: K8sCategories.NODES,
|
||||
showArrow: false,
|
||||
children: (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={GetNodesQuickFiltersConfig(dotMetricsEnabled)}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: renderCategoryLabel(
|
||||
<FilePenLine size={14} className={styles.quickFiltersCategoryLabelIcon} />,
|
||||
'Namespaces',
|
||||
),
|
||||
key: K8sCategories.NAMESPACES,
|
||||
showArrow: false,
|
||||
children: (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={GetNamespaceQuickFiltersConfig(dotMetricsEnabled)}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: renderCategoryLabel(
|
||||
<Boxes size={14} className={styles.quickFiltersCategoryLabelIcon} />,
|
||||
'Clusters',
|
||||
),
|
||||
key: K8sCategories.CLUSTERS,
|
||||
showArrow: false,
|
||||
children: (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={GetClustersQuickFiltersConfig(dotMetricsEnabled)}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: renderCategoryLabel(
|
||||
<Computer size={14} className={styles.quickFiltersCategoryLabelIcon} />,
|
||||
'Deployments',
|
||||
),
|
||||
key: K8sCategories.DEPLOYMENTS,
|
||||
showArrow: false,
|
||||
children: (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={GetDeploymentsQuickFiltersConfig(dotMetricsEnabled)}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: renderCategoryLabel(
|
||||
<Bolt size={14} className={styles.quickFiltersCategoryLabelIcon} />,
|
||||
'Jobs',
|
||||
),
|
||||
key: K8sCategories.JOBS,
|
||||
showArrow: false,
|
||||
children: (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={GetJobsQuickFiltersConfig(dotMetricsEnabled)}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: renderCategoryLabel(
|
||||
<Group size={14} className={styles.quickFiltersCategoryLabelIcon} />,
|
||||
'DaemonSets',
|
||||
),
|
||||
key: K8sCategories.DAEMONSETS,
|
||||
showArrow: false,
|
||||
children: (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={GetDaemonsetsQuickFiltersConfig(dotMetricsEnabled)}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: renderCategoryLabel(
|
||||
<ArrowUpDown size={14} className={styles.quickFiltersCategoryLabelIcon} />,
|
||||
'StatefulSets',
|
||||
),
|
||||
key: K8sCategories.STATEFULSETS,
|
||||
showArrow: false,
|
||||
children: (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={GetStatefulsetsQuickFiltersConfig(dotMetricsEnabled)}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: renderCategoryLabel(
|
||||
<HardDrive size={14} className={styles.quickFiltersCategoryLabelIcon} />,
|
||||
'Volumes',
|
||||
),
|
||||
key: K8sCategories.VOLUMES,
|
||||
showArrow: false,
|
||||
children: (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={GetVolumesQuickFiltersConfig(dotMetricsEnabled)}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
// TODO: Enable once we have implemented containers.
|
||||
// {
|
||||
// label: (
|
||||
// <div className="k8s-quick-filters-category-label">
|
||||
// <div className="k8s-quick-filters-category-label-container">
|
||||
// <PackageOpen
|
||||
// size={14}
|
||||
// className="k8s-quick-filters-category-label-icon"
|
||||
// />
|
||||
// <Typography.Text>Containers</Typography.Text>
|
||||
// </div>
|
||||
// </div>
|
||||
// ),
|
||||
// key: K8sCategories.CONTAINERS,
|
||||
// showArrow: false,
|
||||
// children: (
|
||||
// <QuickFilters
|
||||
// source={QuickFiltersSource.INFRA_MONITORING}
|
||||
// config={ContainersQuickFiltersConfig}
|
||||
// handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
// onFilterChange={handleFilterChange}
|
||||
// />
|
||||
// ),
|
||||
// },
|
||||
];
|
||||
const selectedCategoryConfig = useMemo(
|
||||
() => categories.find((cat) => cat.key === selectedCategory)?.config,
|
||||
[categories, selectedCategory],
|
||||
);
|
||||
|
||||
const handleCategoryChange = (key: string | string[]): void => {
|
||||
if (Array.isArray(key) && key.length > 0) {
|
||||
setSelectedCategory(key[0] as string);
|
||||
const handleCategorySelect = (key: string): void => {
|
||||
if (key !== selectedCategory) {
|
||||
setSelectedCategory(key);
|
||||
// Reset filters
|
||||
setUrlFilters(null);
|
||||
setOrderBy(null);
|
||||
@@ -331,28 +229,71 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
<div className={styles.infraMonitoringContainer}>
|
||||
<div className={styles.infraContentRow}>
|
||||
{showFilters && (
|
||||
<div className={styles.quickFiltersContainer}>
|
||||
<div className={styles.quickFiltersContainerHeader}>
|
||||
<Typography.Text>Filters</Typography.Text>
|
||||
|
||||
<Tooltip title="Collapse Filters">
|
||||
<ArrowUpToLine
|
||||
style={{ transform: 'rotate(270deg)' }}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
size="md"
|
||||
/>
|
||||
</Tooltip>
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
|
||||
initialWidth={quickFiltersInitialWidth}
|
||||
minWidth={QUICK_FILTERS_MIN_WIDTH}
|
||||
maxWidth={QUICK_FILTERS_MAX_WIDTH}
|
||||
onResize={persistQuickFiltersWidth}
|
||||
resetToDefaultOnDoubleClick
|
||||
withHandle
|
||||
className={styles.quickFiltersContainer}
|
||||
handleTestId="quick-filters-resize-handle"
|
||||
>
|
||||
<div className={styles.categorySelectorSection}>
|
||||
<div className={styles.sectionHeader} data-type="resource">
|
||||
<Typography.Text className={styles.sectionLabel}>
|
||||
Viewing · Resource
|
||||
</Typography.Text>
|
||||
<div className={styles.sectionLine} />
|
||||
<Tooltip title="Collapse Filters">
|
||||
<ArrowUpToLine
|
||||
style={{ transform: 'rotate(270deg)' }}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
size="md"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className={styles.categoryCard}>
|
||||
<div className={styles.categoryList}>
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category.key}
|
||||
type="button"
|
||||
className={`${styles.categoryItem} ${
|
||||
selectedCategory === category.key
|
||||
? styles.categoryItemSelected
|
||||
: ''
|
||||
}`}
|
||||
onClick={(): void => handleCategorySelect(category.key)}
|
||||
data-testid={`category-${category.key}`}
|
||||
>
|
||||
{category.icon}
|
||||
<Typography.Text>{category.label}</Typography.Text>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Collapse
|
||||
onChange={handleCategoryChange}
|
||||
items={items}
|
||||
defaultActiveKey={[selectedCategory]}
|
||||
activeKey={[selectedCategory]}
|
||||
accordion
|
||||
bordered={false}
|
||||
ghost
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.quickFiltersSection}>
|
||||
<div className={styles.sectionHeader} data-type="filter">
|
||||
<Typography.Text className={styles.sectionLabel}>
|
||||
Filter by
|
||||
</Typography.Text>
|
||||
<div className={styles.sectionLine} />
|
||||
</div>
|
||||
{selectedCategoryConfig && (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={selectedCategoryConfig}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ResizableBox>
|
||||
)}
|
||||
|
||||
<div
|
||||
|
||||
@@ -1,27 +1,7 @@
|
||||
.llmObservability {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-12) var(--spacing-16);
|
||||
}
|
||||
|
||||
.pageHeader {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.pageHeaderTitle {
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: var(--spacing-2) 0 0;
|
||||
color: var(--text-vanilla-400);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
height: 100%;
|
||||
margin-top: var(--spacing-2);
|
||||
margin-left: var(--spacing-2);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { Tabs } from '@signozhq/ui/tabs';
|
||||
|
||||
import { useLLMObservabilityTabs } from './hooks/useLLMObservabilityTabs';
|
||||
import styles from './LLMObservability.module.scss';
|
||||
|
||||
// Shell for the LLM Observability page: renders the top-level tab bar
|
||||
// (Overview / Configuration) using the SigNoz design-system Tabs, with
|
||||
// route-driven active state from useLLMObservabilityTabs.
|
||||
function LLMObservability(): JSX.Element {
|
||||
const { items, activeTab, onTabChange } = useLLMObservabilityTabs();
|
||||
|
||||
return (
|
||||
<div className={styles.llmObservability} data-testid="llm-observability-page">
|
||||
<header className={styles.pageHeader}>
|
||||
<div className={styles.pageHeaderTitle}>
|
||||
<h1 className={styles.title}>LLM Observability</h1>
|
||||
<p className={styles.subtitle}>
|
||||
Monitor and analyze your LLM usage, costs, and performance
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
<Tabs
|
||||
items={items}
|
||||
value={activeTab}
|
||||
onChange={onTabChange}
|
||||
testId="llm-observability-tabs"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
.overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-12) var(--spacing-16);
|
||||
}
|
||||
|
||||
.pageHeader {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.pageHeaderTitle {
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: var(--spacing-2) 0 0;
|
||||
color: var(--text-vanilla-400);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import styles from './Overview.module.scss';
|
||||
|
||||
// Overview tab content for LLM Observability. Currently the feature's landing
|
||||
// surface; usage/cost/performance widgets land in later PRs.
|
||||
function Overview(): JSX.Element {
|
||||
return (
|
||||
<div className={styles.overview} data-testid="llm-observability-overview">
|
||||
<header className={styles.pageHeader}>
|
||||
<div className={styles.pageHeaderTitle}>
|
||||
<h1 className={styles.title}>LLM Observability</h1>
|
||||
<p className={styles.subtitle}>
|
||||
Monitor and analyze your LLM usage, costs, and performance
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Overview;
|
||||
@@ -2,29 +2,4 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-12) var(--spacing-16);
|
||||
}
|
||||
|
||||
.pageHeader {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.pageHeaderTitle {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2);
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: var(--spacing-2) 0 0;
|
||||
color: var(--text-vanilla-400);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Tabs } from '@signozhq/ui/tabs';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import ModelCostTabPanel from './ModelCostTabPanel';
|
||||
import styles from './LLMObservabilityModelPricing.module.scss';
|
||||
@@ -10,20 +9,9 @@ function LLMObservabilityModelPricing(): JSX.Element {
|
||||
className={styles.llmObservabilityModelPricing}
|
||||
data-testid="llm-observability-model-pricing-page"
|
||||
>
|
||||
<header className={styles.pageHeader}>
|
||||
<div className={styles.pageHeaderTitle}>
|
||||
<Typography.Text as="h1" size="large" weight="semibold">
|
||||
Configuration
|
||||
</Typography.Text>
|
||||
<Typography.Text color="muted">
|
||||
Model pricing and cost estimation settings
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<Tabs
|
||||
// Model costs is the only enabled tab for now, so default to it. When
|
||||
// the unpriced-models tab lands, this can become a URL-backed param.
|
||||
// the unpriced-models tab lands in a later PR.
|
||||
defaultValue="model-costs"
|
||||
items={[
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useMemo } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { SelectSimple } from '@signozhq/ui/select';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Plus, Search, X } from '@signozhq/icons';
|
||||
import { useListLLMPricingRules } from 'api/generated/services/llmpricingrules';
|
||||
import { type ListLLMPricingRulesParams } from 'api/generated/services/sigNoz.schemas';
|
||||
@@ -161,12 +160,6 @@ function ModelCostTabPanel(): JSX.Element {
|
||||
onDelete={deletion.requestDelete}
|
||||
/>
|
||||
|
||||
<footer>
|
||||
<Typography.Text color="muted" size="small">
|
||||
All prices per 1M tokens (USD)
|
||||
</Typography.Text>
|
||||
</footer>
|
||||
|
||||
{drawer.isOpen && (
|
||||
<ModelCostDrawer
|
||||
isOpen={drawer.isOpen}
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
import { LlmpricingruletypesLLMPricingRuleUnitDTO as UnitDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
TOAST_MODEL_COST_DELETED,
|
||||
TOAST_MODEL_COST_UPDATED,
|
||||
} from 'container/LLMObservability/Settings/ModelPricing/constants';
|
||||
import {
|
||||
LLM_PRICING_ENDPOINT,
|
||||
LLM_PRICING_RULE_ENDPOINT,
|
||||
makeListResponse,
|
||||
mockRules,
|
||||
} from 'container/LLMObservability/Settings/ModelPricing/__tests__/fixtures';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
|
||||
|
||||
import ModelCostTabPanel from '../ModelCostTabPanel';
|
||||
|
||||
const toastSuccess = jest.fn();
|
||||
const toastError = jest.fn();
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: {
|
||||
success: (...args: unknown[]): void => toastSuccess(...args),
|
||||
error: (...args: unknown[]): void => toastError(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
function setupList(items = mockRules, total = items.length): void {
|
||||
server.use(
|
||||
rest.get(LLM_PRICING_ENDPOINT, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(makeListResponse(items, total))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function resetUrl(): void {
|
||||
window.history.pushState(null, '', '/');
|
||||
}
|
||||
|
||||
async function openRowMenu(
|
||||
user: ReturnType<typeof userEvent.setup>,
|
||||
ruleId: string,
|
||||
): Promise<void> {
|
||||
const row = screen.getByTestId(`model-cell-name-${ruleId}`).closest('tr');
|
||||
await user.click(within(row as HTMLElement).getByRole('button'));
|
||||
}
|
||||
|
||||
describe('ModelCostTabPanel (integration)', () => {
|
||||
beforeEach(() => {
|
||||
resetUrl();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('renders pricing rules returned by the list API', async () => {
|
||||
setupList();
|
||||
render(<ModelCostTabPanel />);
|
||||
|
||||
const openaiCell = await screen.findByTestId('model-cell-name-rule-openai');
|
||||
expect(openaiCell).toHaveTextContent('gpt-4o');
|
||||
expect(
|
||||
screen.getByTestId('model-cell-name-rule-anthropic'),
|
||||
).toHaveTextContent('claude-3-5-sonnet');
|
||||
// Canonical id under the model name + provider column.
|
||||
expect(screen.getByText('openai:gpt-4o')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('OpenAI').length).toBeGreaterThan(0);
|
||||
// Source badges reflect the override flag.
|
||||
expect(screen.getByTestId('source-badge-rule-openai')).toHaveTextContent(
|
||||
'User override',
|
||||
);
|
||||
expect(screen.getByTestId('source-badge-rule-anthropic')).toHaveTextContent(
|
||||
'Auto',
|
||||
);
|
||||
});
|
||||
|
||||
it('shows the empty state when there are no rules', async () => {
|
||||
setupList([], 0);
|
||||
render(<ModelCostTabPanel />);
|
||||
|
||||
const empty = await screen.findByTestId('model-costs-empty');
|
||||
expect(empty).toHaveTextContent('No model costs yet.');
|
||||
});
|
||||
|
||||
it('shows an error message when the list request fails', async () => {
|
||||
server.use(
|
||||
rest.get(LLM_PRICING_ENDPOINT, (_req, res, ctx) => res(ctx.status(500))),
|
||||
);
|
||||
render(<ModelCostTabPanel />);
|
||||
|
||||
const alert = await screen.findByRole('alert');
|
||||
expect(alert).toHaveTextContent(
|
||||
'Failed to load pricing rules. Please try again.',
|
||||
);
|
||||
});
|
||||
|
||||
it('sends the debounced search term as the q param', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
let lastParams: URLSearchParams | null = null;
|
||||
server.use(
|
||||
rest.get(LLM_PRICING_ENDPOINT, (req, res, ctx) => {
|
||||
lastParams = req.url.searchParams;
|
||||
return res(ctx.status(200), ctx.json(makeListResponse(mockRules)));
|
||||
}),
|
||||
);
|
||||
render(<ModelCostTabPanel />);
|
||||
|
||||
await screen.findByTestId('model-cell-name-rule-openai');
|
||||
await user.type(
|
||||
screen.getByPlaceholderText('Search by model or provider'),
|
||||
'claude',
|
||||
);
|
||||
|
||||
await waitFor(() => expect(lastParams?.get('q')).toBe('claude'));
|
||||
});
|
||||
|
||||
it('clears the search via the clear button', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
setupList();
|
||||
render(<ModelCostTabPanel />);
|
||||
|
||||
const input = screen.getByPlaceholderText(
|
||||
'Search by model or provider',
|
||||
) as HTMLInputElement;
|
||||
await user.type(input, 'gpt');
|
||||
expect(input.value).toBe('gpt');
|
||||
|
||||
await user.click(screen.getByTestId('model-cost-search-clear'));
|
||||
await waitFor(() => expect(input.value).toBe(''));
|
||||
});
|
||||
|
||||
it('sends isOverride=true when the source filter is set to User override', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
let lastParams: URLSearchParams | null = null;
|
||||
server.use(
|
||||
rest.get(LLM_PRICING_ENDPOINT, (req, res, ctx) => {
|
||||
lastParams = req.url.searchParams;
|
||||
return res(ctx.status(200), ctx.json(makeListResponse(mockRules)));
|
||||
}),
|
||||
);
|
||||
render(<ModelCostTabPanel />);
|
||||
|
||||
await screen.findByTestId('model-cell-name-rule-openai');
|
||||
await user.click(screen.getByTestId('source-filter'));
|
||||
// Scope to the listbox option — "User override" also appears as a row badge.
|
||||
await user.click(
|
||||
await screen.findByRole('option', { name: 'User override' }),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(lastParams?.get('isOverride')).toBe('true'));
|
||||
});
|
||||
|
||||
it('opens the add drawer for a manager (ADMIN)', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
setupList();
|
||||
render(<ModelCostTabPanel />);
|
||||
|
||||
await screen.findByTestId('model-cell-name-rule-openai');
|
||||
await user.click(screen.getByTestId('add-model-cost-btn'));
|
||||
|
||||
const modelInput = await screen.findByTestId('drawer-model-id-input');
|
||||
expect(modelInput).toBeInTheDocument();
|
||||
expect(screen.getByTestId('drawer-save-btn')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides the add button and row actions for a viewer', async () => {
|
||||
setupList();
|
||||
render(<ModelCostTabPanel />, undefined, { role: 'VIEWER' });
|
||||
|
||||
const row = (
|
||||
await screen.findByTestId('model-cell-name-rule-openai')
|
||||
).closest('tr') as HTMLElement;
|
||||
expect(screen.queryByTestId('add-model-cost-btn')).not.toBeInTheDocument();
|
||||
// View-only rows render no action menu (no buttons in the row).
|
||||
expect(within(row).queryByRole('button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the edit drawer prefilled from the row action menu', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
setupList();
|
||||
render(<ModelCostTabPanel />);
|
||||
|
||||
await screen.findByTestId('model-cell-name-rule-openai');
|
||||
await openRowMenu(user, 'rule-openai');
|
||||
await user.click(await screen.findByText('Edit'));
|
||||
|
||||
const drawerTitle = await screen.findByText('Edit model cost');
|
||||
expect(drawerTitle).toBeInTheDocument();
|
||||
const modelInput = screen.getByTestId(
|
||||
'drawer-model-id-input',
|
||||
) as HTMLInputElement;
|
||||
expect(modelInput.value).toBe('gpt-4o');
|
||||
expect(modelInput).toBeDisabled();
|
||||
});
|
||||
|
||||
it('deletes a rule through the confirm dialog', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
let deletedId: string | null = null;
|
||||
setupList();
|
||||
server.use(
|
||||
rest.delete(LLM_PRICING_RULE_ENDPOINT, (req, res, ctx) => {
|
||||
deletedId = req.params.id as string;
|
||||
return res(ctx.status(200), ctx.json({ status: 'success' }));
|
||||
}),
|
||||
);
|
||||
render(<ModelCostTabPanel />);
|
||||
|
||||
await screen.findByTestId('model-cell-name-rule-openai');
|
||||
await openRowMenu(user, 'rule-openai');
|
||||
await user.click(await screen.findByText('Delete'));
|
||||
|
||||
await user.click(await screen.findByTestId('drawer-delete-confirm-btn'));
|
||||
|
||||
await waitFor(() => expect(deletedId).toBe('rule-openai'));
|
||||
await waitFor(() =>
|
||||
expect(toastSuccess).toHaveBeenCalledWith(TOAST_MODEL_COST_DELETED),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders cache buckets for rules that have cache pricing', async () => {
|
||||
setupList();
|
||||
render(<ModelCostTabPanel />);
|
||||
|
||||
const anthropicRow = (
|
||||
await screen.findByTestId('model-cell-name-rule-anthropic')
|
||||
).closest('tr') as HTMLElement;
|
||||
expect(within(anthropicRow).getByText(/Cache Read/i)).toBeInTheDocument();
|
||||
expect(within(anthropicRow).getByText(/Cache Write/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('formats per-million prices in the row', async () => {
|
||||
setupList();
|
||||
render(<ModelCostTabPanel />);
|
||||
|
||||
const openaiRow = (
|
||||
await screen.findByTestId('model-cell-name-rule-openai')
|
||||
).closest('tr') as HTMLElement;
|
||||
// mockRules gpt-4o has input cost 3 → rendered as $3.00.
|
||||
expect(within(openaiRow).getByText('$3.00')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('sends a normalized create payload when adding a rule', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
let body: Record<string, unknown> | null = null;
|
||||
setupList();
|
||||
server.use(
|
||||
rest.put(LLM_PRICING_ENDPOINT, async (req, res, ctx) => {
|
||||
body = await req.json();
|
||||
return res(ctx.status(200), ctx.json({ status: 'success' }));
|
||||
}),
|
||||
);
|
||||
render(<ModelCostTabPanel />);
|
||||
|
||||
await screen.findByTestId('model-cell-name-rule-openai');
|
||||
await user.click(screen.getByTestId('add-model-cost-btn'));
|
||||
|
||||
// Leading/trailing whitespace should be trimmed off the model id.
|
||||
await user.type(
|
||||
await screen.findByTestId('drawer-model-id-input'),
|
||||
' gpt-4o-mini ',
|
||||
);
|
||||
await user.type(screen.getByTestId('drawer-input-cost'), '3');
|
||||
await user.type(screen.getByTestId('drawer-output-cost'), '9');
|
||||
await user.click(screen.getByTestId('drawer-save-btn'));
|
||||
|
||||
await waitFor(() => expect(body).not.toBeNull());
|
||||
// The create call submits a bulk `rules` array of normalized payloads.
|
||||
const [payload] = (
|
||||
body as unknown as {
|
||||
rules: Record<string, unknown>[];
|
||||
}
|
||||
).rules;
|
||||
expect(payload).toMatchObject({
|
||||
modelName: 'gpt-4o-mini',
|
||||
provider: 'OpenAI',
|
||||
isOverride: true,
|
||||
enabled: true,
|
||||
unit: UnitDTO.per_million_tokens,
|
||||
pricing: { input: 3, output: 9 },
|
||||
});
|
||||
});
|
||||
|
||||
it('sends an updated payload when editing a rule', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
let body: Record<string, unknown> | null = null;
|
||||
setupList();
|
||||
server.use(
|
||||
rest.put(LLM_PRICING_ENDPOINT, async (req, res, ctx) => {
|
||||
body = await req.json();
|
||||
return res(ctx.status(200), ctx.json({ status: 'success' }));
|
||||
}),
|
||||
);
|
||||
render(<ModelCostTabPanel />);
|
||||
|
||||
await screen.findByTestId('model-cell-name-rule-openai');
|
||||
await openRowMenu(user, 'rule-openai');
|
||||
await user.click(await screen.findByText('Edit'));
|
||||
|
||||
// Model id + provider are locked in edit mode; change the prefilled input cost.
|
||||
const inputCost = await screen.findByTestId('drawer-input-cost');
|
||||
await user.clear(inputCost);
|
||||
await user.type(inputCost, '5');
|
||||
await user.click(screen.getByTestId('drawer-save-btn'));
|
||||
|
||||
await waitFor(() => expect(body).not.toBeNull());
|
||||
const [payload] = (
|
||||
body as unknown as {
|
||||
rules: Record<string, unknown>[];
|
||||
}
|
||||
).rules;
|
||||
// Edit carries the rule id; disabled model/provider are still submitted and
|
||||
// the edited price flows through, while output keeps its prefilled value.
|
||||
expect(payload).toMatchObject({
|
||||
id: 'rule-openai',
|
||||
modelName: 'gpt-4o',
|
||||
provider: 'OpenAI',
|
||||
isOverride: true,
|
||||
enabled: true,
|
||||
unit: UnitDTO.per_million_tokens,
|
||||
pricing: { input: 5, output: 9 },
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(toastSuccess).toHaveBeenCalledWith(TOAST_MODEL_COST_UPDATED),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,295 @@
|
||||
import { makePricingRule } from 'container/LLMObservability/Settings/ModelPricing/__tests__/fixtures';
|
||||
import { EMPTY_DRAFT } from 'container/LLMObservability/Settings/ModelPricing/constants';
|
||||
import { draftFromRule } from 'container/LLMObservability/Settings/ModelPricing/utils';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import ModelCostDrawer from '../ModelCostDrawer';
|
||||
|
||||
const editDraft = draftFromRule(
|
||||
makePricingRule({
|
||||
id: 'rule-openai',
|
||||
modelName: 'gpt-4o',
|
||||
provider: 'OpenAI',
|
||||
}),
|
||||
);
|
||||
|
||||
describe('ModelCostDrawer (integration)', () => {
|
||||
it('renders the add title and a save button for a manager', () => {
|
||||
render(
|
||||
<ModelCostDrawer
|
||||
isOpen
|
||||
mode="add"
|
||||
initialDraft={EMPTY_DRAFT}
|
||||
onClose={jest.fn()}
|
||||
onSave={jest.fn()}
|
||||
isSaving={false}
|
||||
saveError={null}
|
||||
canManage
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Add model cost')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('drawer-save-btn')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables save until the form is dirty', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(
|
||||
<ModelCostDrawer
|
||||
isOpen
|
||||
mode="add"
|
||||
initialDraft={EMPTY_DRAFT}
|
||||
onClose={jest.fn()}
|
||||
onSave={jest.fn()}
|
||||
isSaving={false}
|
||||
saveError={null}
|
||||
canManage
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('drawer-save-btn')).toBeDisabled();
|
||||
|
||||
await user.type(screen.getByTestId('drawer-model-id-input'), 'openai:gpt-4o');
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('drawer-save-btn')).toBeEnabled(),
|
||||
);
|
||||
});
|
||||
|
||||
it('shows the model id required error and does not call onSave when the name is empty', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const onSave = jest.fn();
|
||||
render(
|
||||
<ModelCostDrawer
|
||||
isOpen
|
||||
mode="add"
|
||||
initialDraft={EMPTY_DRAFT}
|
||||
onClose={jest.fn()}
|
||||
onSave={onSave}
|
||||
isSaving={false}
|
||||
saveError={null}
|
||||
canManage
|
||||
/>,
|
||||
);
|
||||
|
||||
// Make the form dirty without touching the model id: add a pattern, which
|
||||
// mutates the `patterns` form field while leaving the name empty.
|
||||
await user.type(screen.getByTestId('drawer-pattern-input'), 'gpt');
|
||||
await user.click(screen.getByTestId('drawer-pattern-add-btn'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('drawer-save-btn')).toBeEnabled(),
|
||||
);
|
||||
await user.click(screen.getByTestId('drawer-save-btn'));
|
||||
|
||||
const error = await screen.findByText('Billing model ID is required.');
|
||||
expect(error).toBeInTheDocument();
|
||||
expect(onSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onSave once on the happy path with valid model id and pricing', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const onSave = jest.fn();
|
||||
render(
|
||||
<ModelCostDrawer
|
||||
isOpen
|
||||
mode="add"
|
||||
initialDraft={EMPTY_DRAFT}
|
||||
onClose={jest.fn()}
|
||||
onSave={onSave}
|
||||
isSaving={false}
|
||||
saveError={null}
|
||||
canManage
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.type(screen.getByTestId('drawer-model-id-input'), 'openai:gpt-4o');
|
||||
await user.type(screen.getByTestId('drawer-input-cost'), '3');
|
||||
await user.type(screen.getByTestId('drawer-output-cost'), '9');
|
||||
|
||||
await user.click(screen.getByTestId('drawer-save-btn'));
|
||||
|
||||
await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it('renders the edit title with disabled, prefilled model id and disabled provider', () => {
|
||||
render(
|
||||
<ModelCostDrawer
|
||||
isOpen
|
||||
mode="edit"
|
||||
initialDraft={editDraft}
|
||||
onClose={jest.fn()}
|
||||
onSave={jest.fn()}
|
||||
isSaving={false}
|
||||
saveError={null}
|
||||
canManage
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Edit model cost')).toBeInTheDocument();
|
||||
const modelInput = screen.getByTestId(
|
||||
'drawer-model-id-input',
|
||||
) as HTMLInputElement;
|
||||
expect(modelInput.value).toBe('gpt-4o');
|
||||
expect(modelInput).toBeDisabled();
|
||||
expect(screen.getByTestId('drawer-provider-select')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('renders a read-only view with a Close button and no save for a viewer', () => {
|
||||
render(
|
||||
<ModelCostDrawer
|
||||
isOpen
|
||||
mode="edit"
|
||||
initialDraft={editDraft}
|
||||
onClose={jest.fn()}
|
||||
onSave={jest.fn()}
|
||||
isSaving={false}
|
||||
saveError={null}
|
||||
canManage={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('View model cost')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('drawer-save-btn')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('drawer-cancel-btn')).toHaveTextContent('Close');
|
||||
});
|
||||
|
||||
it('renders the save error text', () => {
|
||||
render(
|
||||
<ModelCostDrawer
|
||||
isOpen
|
||||
mode="add"
|
||||
initialDraft={EMPTY_DRAFT}
|
||||
onClose={jest.fn()}
|
||||
onSave={jest.fn()}
|
||||
isSaving={false}
|
||||
saveError="boom"
|
||||
canManage
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('boom')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('adds and removes a model pattern from the editor', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(
|
||||
<ModelCostDrawer
|
||||
isOpen
|
||||
mode="add"
|
||||
initialDraft={EMPTY_DRAFT}
|
||||
onClose={jest.fn()}
|
||||
onSave={jest.fn()}
|
||||
isSaving={false}
|
||||
saveError={null}
|
||||
canManage
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.type(screen.getByTestId('drawer-pattern-input'), 'gpt-5');
|
||||
await user.click(screen.getByTestId('drawer-pattern-add-btn'));
|
||||
// The added pattern renders as a removable chip.
|
||||
const removeChip = screen.getByRole('button', {
|
||||
name: 'Remove pattern gpt-5',
|
||||
});
|
||||
expect(removeChip).toBeInTheDocument();
|
||||
|
||||
await user.click(removeChip);
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Remove pattern gpt-5' }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('adds a cache pricing bucket via the picker and removes it', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(
|
||||
<ModelCostDrawer
|
||||
isOpen
|
||||
mode="add"
|
||||
initialDraft={EMPTY_DRAFT}
|
||||
onClose={jest.fn()}
|
||||
onSave={jest.fn()}
|
||||
isSaving={false}
|
||||
saveError={null}
|
||||
canManage
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId('drawer-add-bucket-btn'));
|
||||
await user.click(screen.getByTestId('drawer-add-bucket-cache-read'));
|
||||
|
||||
// Adding the bucket reveals its cost input and the shared cache-mode select.
|
||||
expect(screen.getByTestId('drawer-cache-read-cost')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('drawer-cache-mode')).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByTestId('drawer-remove-cache-read'));
|
||||
expect(
|
||||
screen.queryByTestId('drawer-cache-read-cost'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('blocks save with a pricing error when an override rule has no input cost', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const onSave = jest.fn();
|
||||
render(
|
||||
<ModelCostDrawer
|
||||
isOpen
|
||||
mode="add"
|
||||
initialDraft={EMPTY_DRAFT}
|
||||
onClose={jest.fn()}
|
||||
onSave={onSave}
|
||||
isSaving={false}
|
||||
saveError={null}
|
||||
canManage
|
||||
/>,
|
||||
);
|
||||
|
||||
// EMPTY_DRAFT defaults to an override with empty pricing. Fill only the
|
||||
// model id + output cost so the form is dirty but the input cost is missing.
|
||||
await user.type(screen.getByTestId('drawer-model-id-input'), 'openai:gpt-4o');
|
||||
await user.type(screen.getByTestId('drawer-output-cost'), '9');
|
||||
await user.click(screen.getByTestId('drawer-save-btn'));
|
||||
|
||||
await expect(
|
||||
screen.findByText('Input cost must be greater than 0.'),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(onSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('requires confirmation to reset an override rule back to auto', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(
|
||||
<ModelCostDrawer
|
||||
isOpen
|
||||
mode="edit"
|
||||
initialDraft={editDraft}
|
||||
onClose={jest.fn()}
|
||||
onSave={jest.fn()}
|
||||
isSaving={false}
|
||||
saveError={null}
|
||||
canManage
|
||||
/>,
|
||||
);
|
||||
|
||||
// Pricing is editable while the rule is an override.
|
||||
expect(screen.getByTestId('drawer-input-cost')).toBeEnabled();
|
||||
|
||||
// Picking "auto" surfaces a confirm step instead of applying immediately.
|
||||
await user.click(screen.getByTestId('drawer-source-auto'));
|
||||
expect(screen.getByTestId('drawer-reset-confirm-btn')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('drawer-input-cost')).toBeEnabled();
|
||||
|
||||
// Keep backs out of the reset.
|
||||
await user.click(screen.getByTestId('drawer-reset-keep-btn'));
|
||||
expect(
|
||||
screen.queryByTestId('drawer-reset-confirm-btn'),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Confirming the reset flips the rule to auto and locks pricing.
|
||||
await user.click(screen.getByTestId('drawer-source-auto'));
|
||||
await user.click(screen.getByTestId('drawer-reset-confirm-btn'));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('drawer-input-cost')).toBeDisabled(),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
useCreateOrUpdateLLMPricingRules,
|
||||
} from 'api/generated/services/llmpricingrules';
|
||||
|
||||
import { EMPTY_DRAFT } from '../../../../constants';
|
||||
import {
|
||||
EMPTY_DRAFT,
|
||||
TOAST_MODEL_COST_ADDED,
|
||||
TOAST_MODEL_COST_UPDATED,
|
||||
} from '../../../../constants';
|
||||
import type { DrawerDraft, DrawerMode, PricingRule } from '../../../../types';
|
||||
import { buildRulePayload, draftFromRule } from '../../../../utils';
|
||||
|
||||
@@ -76,7 +80,9 @@ export function useModelCostDrawer(): UseModelCostDrawerResult {
|
||||
await invalidateList();
|
||||
setIsOpen(false);
|
||||
setSelectedRuleId(null);
|
||||
toast.success(mode === 'edit' ? 'Model cost updated' : 'Model cost added');
|
||||
toast.success(
|
||||
mode === 'edit' ? TOAST_MODEL_COST_UPDATED : TOAST_MODEL_COST_ADDED,
|
||||
);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Save failed';
|
||||
setSaveError(message);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.modelCostsTable {
|
||||
margin-top: var(--spacing-8);
|
||||
--tanstack-table-row-height: 48px;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 170px);
|
||||
overflow-y: auto;
|
||||
|
||||
:global(table) tbody tr {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useDeleteLLMPricingRule,
|
||||
} from 'api/generated/services/llmpricingrules';
|
||||
|
||||
import { TOAST_MODEL_COST_DELETED } from '../../constants';
|
||||
import type { PricingRule } from '../../types';
|
||||
|
||||
// The minimal slice of a rule the delete-confirm flow needs: the id to delete
|
||||
@@ -49,7 +50,7 @@ export function useModelCostDelete(): UseModelCostDeleteResult {
|
||||
queryKey: getListLLMPricingRulesQueryKey(),
|
||||
});
|
||||
setPendingDelete(null);
|
||||
toast.success('Model cost deleted');
|
||||
toast.success(TOAST_MODEL_COST_DELETED);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Delete failed';
|
||||
toast.error(message);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
LlmpricingruletypesLLMPricingRuleCacheModeDTO as CacheModeDTO,
|
||||
LlmpricingruletypesLLMPricingRuleUnitDTO as UnitDTO,
|
||||
type ListLLMPricingRules200,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import type { PricingRule } from '../types';
|
||||
|
||||
// Endpoint glob used by MSW handlers. The generated client hits a relative
|
||||
// `/api/v1/llm_pricing_rules`, so the `*` prefix matches regardless of base URL.
|
||||
export const LLM_PRICING_ENDPOINT = '*/api/v1/llm_pricing_rules';
|
||||
export const LLM_PRICING_RULE_ENDPOINT = '*/api/v1/llm_pricing_rules/:id';
|
||||
|
||||
// Builds a valid pricing rule, with overrides merged shallowly. Pricing is
|
||||
// replaced wholesale when provided so callers can shape cache buckets freely.
|
||||
export function makePricingRule(
|
||||
overrides: Partial<PricingRule> = {},
|
||||
): PricingRule {
|
||||
const { pricing, ...rest } = overrides;
|
||||
return {
|
||||
id: 'rule-1',
|
||||
enabled: true,
|
||||
isOverride: true,
|
||||
modelName: 'gpt-4o',
|
||||
modelPattern: ['gpt-4o'],
|
||||
orgId: 'org-1',
|
||||
provider: 'OpenAI',
|
||||
sourceId: 'source-1',
|
||||
unit: UnitDTO.per_million_tokens,
|
||||
createdAt: '2023-10-01T00:00:00.000Z',
|
||||
updatedAt: '2023-10-10T00:00:00.000Z',
|
||||
syncedAt: '2023-10-10T00:00:00.000Z',
|
||||
pricing: {
|
||||
input: 3,
|
||||
output: 9,
|
||||
...pricing,
|
||||
},
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
export const mockRules: PricingRule[] = [
|
||||
makePricingRule({
|
||||
id: 'rule-openai',
|
||||
modelName: 'gpt-4o',
|
||||
provider: 'OpenAI',
|
||||
isOverride: true,
|
||||
pricing: { input: 3, output: 9 },
|
||||
}),
|
||||
makePricingRule({
|
||||
id: 'rule-anthropic',
|
||||
modelName: 'claude-3-5-sonnet',
|
||||
provider: 'Anthropic',
|
||||
isOverride: false,
|
||||
pricing: {
|
||||
input: 2,
|
||||
output: 30,
|
||||
cache: { mode: CacheModeDTO.additive, read: 3, write: 6 },
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
// Wraps items in the list response envelope the list query reads
|
||||
// (`data.data.items` / `data.data.total`).
|
||||
export function makeListResponse(
|
||||
items: PricingRule[],
|
||||
total = items.length,
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
): ListLLMPricingRules200 {
|
||||
return {
|
||||
status: 'success',
|
||||
data: {
|
||||
items,
|
||||
total,
|
||||
offset,
|
||||
limit,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -4,6 +4,10 @@ import type { CacheBucketDef, DrawerDraft } from './types';
|
||||
|
||||
export const PAGE_SIZE = 20;
|
||||
|
||||
export const TOAST_MODEL_COST_ADDED = 'Model cost added';
|
||||
export const TOAST_MODEL_COST_UPDATED = 'Model cost updated';
|
||||
export const TOAST_MODEL_COST_DELETED = 'Model cost deleted';
|
||||
|
||||
export const PAGE_KEY = 'page';
|
||||
export const LIMIT_KEY = 'limit';
|
||||
export const SEARCH_KEY = 'search';
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { safeNavigateMock } from '__tests__/safeNavigateMock';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
LLM_PRICING_ENDPOINT,
|
||||
makeListResponse,
|
||||
mockRules,
|
||||
} from 'container/LLMObservability/Settings/ModelPricing/__tests__/fixtures';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import LLMObservability from '../LLMObservability';
|
||||
|
||||
function setupList(items = mockRules): void {
|
||||
server.use(
|
||||
rest.get(LLM_PRICING_ENDPOINT, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(makeListResponse(items))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
describe('LLMObservability (integration)', () => {
|
||||
beforeEach(() => {
|
||||
window.history.pushState(null, '', '/');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('renders the overview panel and the tab bar on the overview route', () => {
|
||||
render(<LLMObservability />, undefined, {
|
||||
initialRoute: ROUTES.LLM_OBSERVABILITY_OVERVIEW,
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('llm-observability-tabs')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('llm-observability-overview')).toBeInTheDocument();
|
||||
expect(screen.getByText('LLM Observability')).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Overview' })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('tab', { name: 'Model pricing' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('navigates to the configuration route when the Model pricing tab is clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<LLMObservability />, undefined, {
|
||||
initialRoute: ROUTES.LLM_OBSERVABILITY_OVERVIEW,
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('tab', { name: 'Model pricing' }));
|
||||
|
||||
expect(safeNavigateMock).toHaveBeenCalledWith(
|
||||
ROUTES.LLM_OBSERVABILITY_CONFIGURATION,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the model-pricing page on the configuration route', async () => {
|
||||
setupList();
|
||||
render(<LLMObservability />, undefined, {
|
||||
initialRoute: ROUTES.LLM_OBSERVABILITY_CONFIGURATION,
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByTestId('llm-observability-model-pricing-page'),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { type TabItemProps } from '@signozhq/ui/tabs';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
|
||||
import Overview from '../Overview/Overview';
|
||||
import LLMObservabilityModelPricing from '../Settings/ModelPricing/LLMObservabilityModelPricing';
|
||||
|
||||
const OVERVIEW_KEY = ROUTES.LLM_OBSERVABILITY_OVERVIEW;
|
||||
const CONFIGURATION_KEY = ROUTES.LLM_OBSERVABILITY_CONFIGURATION;
|
||||
|
||||
interface UseLLMObservabilityTabsResult {
|
||||
items: TabItemProps[];
|
||||
activeTab: string;
|
||||
onTabChange: (key: string) => void;
|
||||
}
|
||||
|
||||
// Drives the top-level LLM Observability tabs. Route-driven: the active tab is
|
||||
// derived from the pathname (each tab owns a URL) and changing tabs navigates,
|
||||
// so tabs stay shareable/back-button friendly while rendering with the SigNoz
|
||||
// design-system Tabs.
|
||||
export function useLLMObservabilityTabs(): UseLLMObservabilityTabsResult {
|
||||
const { pathname } = useLocation();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const activeTab = pathname.startsWith(CONFIGURATION_KEY)
|
||||
? CONFIGURATION_KEY
|
||||
: OVERVIEW_KEY;
|
||||
|
||||
const onTabChange = useCallback(
|
||||
(key: string): void => {
|
||||
safeNavigate(key);
|
||||
},
|
||||
[safeNavigate],
|
||||
);
|
||||
|
||||
const items: TabItemProps[] = [
|
||||
{
|
||||
key: OVERVIEW_KEY,
|
||||
label: 'Overview',
|
||||
children: <Overview />,
|
||||
},
|
||||
{
|
||||
key: CONFIGURATION_KEY,
|
||||
label: 'Model pricing',
|
||||
children: <LLMObservabilityModelPricing />,
|
||||
},
|
||||
];
|
||||
|
||||
return { items, activeTab, onTabChange };
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { ChangeEvent, useState } from 'react';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import ApacheIcon from 'assets/CustomIcons/ApacheIcon';
|
||||
import DockerIcon from 'assets/CustomIcons/DockerIcon';
|
||||
import ElasticSearchIcon from 'assets/CustomIcons/ElasticSearchIcon';
|
||||
import HerokuIcon from 'assets/CustomIcons/HerokuIcon';
|
||||
import KubernetesIcon from 'assets/CustomIcons/KubernetesIcon';
|
||||
import MongoDBIcon from 'assets/CustomIcons/MongoDBIcon';
|
||||
import MySQLIcon from 'assets/CustomIcons/MySQLIcon';
|
||||
import NginxIcon from 'assets/CustomIcons/NginxIcon';
|
||||
import PostgreSQLIcon from 'assets/CustomIcons/PostgreSQLIcon';
|
||||
import RedisIcon from 'assets/CustomIcons/RedisIcon';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
ConciergeBell,
|
||||
DraftingCompass,
|
||||
Drill,
|
||||
Plus,
|
||||
X,
|
||||
} from '@signozhq/icons';
|
||||
import { DashboardTemplate } from 'types/api/dashboard/getAll';
|
||||
|
||||
import blankDashboardTemplatePreviewUrl from '@/assets/Images/blankDashboardTemplatePreview.svg';
|
||||
import redisTemplatePreviewUrl from '@/assets/Images/redisTemplatePreview.svg';
|
||||
|
||||
import { filterTemplates } from '../utils';
|
||||
|
||||
import './DashboardTemplatesModal.styles.scss';
|
||||
|
||||
const templatesList: DashboardTemplate[] = [
|
||||
{
|
||||
name: 'Blank dashboard',
|
||||
icon: <Drill />,
|
||||
id: 'blank',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'Alert Manager',
|
||||
icon: <ConciergeBell />,
|
||||
id: 'alertManager',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'Apache',
|
||||
icon: <ApacheIcon />,
|
||||
id: 'apache',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'Docker',
|
||||
icon: <DockerIcon />,
|
||||
id: 'docker',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'Elasticsearch',
|
||||
icon: <ElasticSearchIcon />,
|
||||
id: 'elasticSearch',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'MongoDB',
|
||||
icon: <MongoDBIcon />,
|
||||
id: 'mongoDB',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'Heroku',
|
||||
icon: <HerokuIcon />,
|
||||
id: 'heroku',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'Nginx',
|
||||
icon: <NginxIcon />,
|
||||
id: 'nginx',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'Kubernetes',
|
||||
icon: <KubernetesIcon />,
|
||||
id: 'kubernetes',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'MySQL',
|
||||
icon: <MySQLIcon />,
|
||||
id: 'mySQL',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'PostgreSQL',
|
||||
icon: <PostgreSQLIcon />,
|
||||
id: 'postgreSQL',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'Redis',
|
||||
icon: <RedisIcon />,
|
||||
id: 'redis',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: redisTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'AWS',
|
||||
icon: <DraftingCompass size={14} />,
|
||||
id: 'aws',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
];
|
||||
|
||||
interface DashboardTemplatesContentProps {
|
||||
onCreateNewDashboard: () => void;
|
||||
/** When provided, renders the modal-style header with a close affordance. Omitted for inline use. */
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
// The template gallery (search + list + preview + create), extracted from the
|
||||
// modal so it can be embedded inline (e.g. the V2 new-dashboard modal's template
|
||||
// tab) as well as inside DashboardTemplatesModal. Styles live under the global
|
||||
// `.new-dashboard-templates-modal` scope, so inline callers wrap it in that class.
|
||||
export default function DashboardTemplatesContent({
|
||||
onCreateNewDashboard,
|
||||
onCancel,
|
||||
}: DashboardTemplatesContentProps): JSX.Element {
|
||||
const [selectedDashboardTemplate, setSelectedDashboardTemplate] = useState(
|
||||
templatesList[0],
|
||||
);
|
||||
|
||||
const [dashboardTemplates, setDashboardTemplates] = useState(templatesList);
|
||||
|
||||
const handleDashboardTemplateSearch = (
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const searchText = event.target.value;
|
||||
const filteredTemplates = filterTemplates(searchText, templatesList);
|
||||
setDashboardTemplates(filteredTemplates);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="new-dashboard-templates-content-container">
|
||||
{onCancel && (
|
||||
<div className="new-dashboard-templates-content-header">
|
||||
<Typography.Text>New Dashboard</Typography.Text>
|
||||
|
||||
<X size={14} className="periscope-btn ghost" onClick={onCancel} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="new-dashboard-templates-content">
|
||||
<div className="new-dashboard-templates-list">
|
||||
<Input
|
||||
className="new-dashboard-templates-search"
|
||||
placeholder="🔍 Search..."
|
||||
onChange={handleDashboardTemplateSearch}
|
||||
/>
|
||||
|
||||
<div className="templates-list">
|
||||
{dashboardTemplates.map((template) => (
|
||||
<div
|
||||
className={cx(
|
||||
'template-list-item',
|
||||
selectedDashboardTemplate.id === template.id ? 'active' : '',
|
||||
)}
|
||||
key={template.name}
|
||||
onClick={() => setSelectedDashboardTemplate(template)}
|
||||
>
|
||||
<div className="template-icon">{template.icon}</div>
|
||||
<div className="template-name">{template.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="new-dashboard-template-preview">
|
||||
<div className="template-preview-header">
|
||||
<div className="template-preview-title">
|
||||
<div className="template-preview-icon">
|
||||
{selectedDashboardTemplate.icon}
|
||||
</div>
|
||||
|
||||
<div className="template-info">
|
||||
<div className="template-name">{selectedDashboardTemplate.name}</div>
|
||||
|
||||
<div className="template-description">
|
||||
{selectedDashboardTemplate.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="create-dashboard-btn">
|
||||
<Button
|
||||
type="primary"
|
||||
className="periscope-btn primary"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={onCreateNewDashboard}
|
||||
>
|
||||
New dashboard
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="template-preview-image">
|
||||
<img
|
||||
src={selectedDashboardTemplate.previewImage}
|
||||
alt={`${selectedDashboardTemplate.name}-preview`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,129 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { ChangeEvent, useState } from 'react';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Modal } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import ApacheIcon from 'assets/CustomIcons/ApacheIcon';
|
||||
import DockerIcon from 'assets/CustomIcons/DockerIcon';
|
||||
import ElasticSearchIcon from 'assets/CustomIcons/ElasticSearchIcon';
|
||||
import HerokuIcon from 'assets/CustomIcons/HerokuIcon';
|
||||
import KubernetesIcon from 'assets/CustomIcons/KubernetesIcon';
|
||||
import MongoDBIcon from 'assets/CustomIcons/MongoDBIcon';
|
||||
import MySQLIcon from 'assets/CustomIcons/MySQLIcon';
|
||||
import NginxIcon from 'assets/CustomIcons/NginxIcon';
|
||||
import PostgreSQLIcon from 'assets/CustomIcons/PostgreSQLIcon';
|
||||
import RedisIcon from 'assets/CustomIcons/RedisIcon';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
ConciergeBell,
|
||||
DraftingCompass,
|
||||
Drill,
|
||||
Plus,
|
||||
X,
|
||||
} from '@signozhq/icons';
|
||||
import { DashboardTemplate } from 'types/api/dashboard/getAll';
|
||||
import { Modal } from 'antd';
|
||||
|
||||
import blankDashboardTemplatePreviewUrl from '@/assets/Images/blankDashboardTemplatePreview.svg';
|
||||
import redisTemplatePreviewUrl from '@/assets/Images/redisTemplatePreview.svg';
|
||||
|
||||
import { filterTemplates } from '../utils';
|
||||
import DashboardTemplatesContent from './DashboardTemplatesContent';
|
||||
|
||||
import './DashboardTemplatesModal.styles.scss';
|
||||
|
||||
const templatesList: DashboardTemplate[] = [
|
||||
{
|
||||
name: 'Blank dashboard',
|
||||
icon: <Drill />,
|
||||
id: 'blank',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'Alert Manager',
|
||||
icon: <ConciergeBell />,
|
||||
id: 'alertManager',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'Apache',
|
||||
icon: <ApacheIcon />,
|
||||
id: 'apache',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'Docker',
|
||||
icon: <DockerIcon />,
|
||||
id: 'docker',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'Elasticsearch',
|
||||
icon: <ElasticSearchIcon />,
|
||||
id: 'elasticSearch',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'MongoDB',
|
||||
icon: <MongoDBIcon />,
|
||||
id: 'mongoDB',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'Heroku',
|
||||
icon: <HerokuIcon />,
|
||||
id: 'heroku',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'Nginx',
|
||||
icon: <NginxIcon />,
|
||||
id: 'nginx',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'Kubernetes',
|
||||
icon: <KubernetesIcon />,
|
||||
id: 'kubernetes',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'MySQL',
|
||||
icon: <MySQLIcon />,
|
||||
id: 'mySQL',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'PostgreSQL',
|
||||
icon: <PostgreSQLIcon />,
|
||||
id: 'postgreSQL',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'Redis',
|
||||
icon: <RedisIcon />,
|
||||
id: 'redis',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: redisTemplatePreviewUrl,
|
||||
},
|
||||
{
|
||||
name: 'AWS',
|
||||
icon: <DraftingCompass size={14} />,
|
||||
id: 'aws',
|
||||
description: 'Create a custom dashboard from scratch.',
|
||||
previewImage: blankDashboardTemplatePreviewUrl,
|
||||
},
|
||||
];
|
||||
|
||||
interface DashboardTemplatesModalProps {
|
||||
showNewDashboardTemplatesModal: boolean;
|
||||
onCreateNewDashboard: () => void;
|
||||
@@ -135,20 +15,6 @@ export default function DashboardTemplatesModal({
|
||||
onCreateNewDashboard,
|
||||
onCancel,
|
||||
}: DashboardTemplatesModalProps): JSX.Element {
|
||||
const [selectedDashboardTemplate, setSelectedDashboardTemplate] = useState(
|
||||
templatesList[0],
|
||||
);
|
||||
|
||||
const [dashboardTemplates, setDashboardTemplates] = useState(templatesList);
|
||||
|
||||
const handleDashboardTemplateSearch = (
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const searchText = event.target.value;
|
||||
const filteredTemplates = filterTemplates(searchText, templatesList);
|
||||
setDashboardTemplates(filteredTemplates);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
wrapClassName="new-dashboard-templates-modal"
|
||||
@@ -159,75 +25,10 @@ export default function DashboardTemplatesModal({
|
||||
destroyOnClose
|
||||
width="60vw"
|
||||
>
|
||||
<div className="new-dashboard-templates-content-container">
|
||||
<div className="new-dashboard-templates-content-header">
|
||||
<Typography.Text>New Dashboard</Typography.Text>
|
||||
|
||||
<X size={14} className="periscope-btn ghost" onClick={onCancel} />
|
||||
</div>
|
||||
|
||||
<div className="new-dashboard-templates-content">
|
||||
<div className="new-dashboard-templates-list">
|
||||
<Input
|
||||
className="new-dashboard-templates-search"
|
||||
placeholder="🔍 Search..."
|
||||
onChange={handleDashboardTemplateSearch}
|
||||
/>
|
||||
|
||||
<div className="templates-list">
|
||||
{dashboardTemplates.map((template) => (
|
||||
<div
|
||||
className={cx(
|
||||
'template-list-item',
|
||||
selectedDashboardTemplate.id === template.id ? 'active' : '',
|
||||
)}
|
||||
key={template.name}
|
||||
onClick={() => setSelectedDashboardTemplate(template)}
|
||||
>
|
||||
<div className="template-icon">{template.icon}</div>
|
||||
<div className="template-name">{template.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="new-dashboard-template-preview">
|
||||
<div className="template-preview-header">
|
||||
<div className="template-preview-title">
|
||||
<div className="template-preview-icon">
|
||||
{selectedDashboardTemplate.icon}
|
||||
</div>
|
||||
|
||||
<div className="template-info">
|
||||
<div className="template-name">{selectedDashboardTemplate.name}</div>
|
||||
|
||||
<div className="template-description">
|
||||
{selectedDashboardTemplate.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="create-dashboard-btn">
|
||||
<Button
|
||||
type="primary"
|
||||
className="periscope-btn primary"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={onCreateNewDashboard}
|
||||
>
|
||||
New dashboard
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="template-preview-image">
|
||||
<img
|
||||
src={selectedDashboardTemplate.previewImage}
|
||||
alt={`${selectedDashboardTemplate.name}-preview`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DashboardTemplatesContent
|
||||
onCreateNewDashboard={onCreateNewDashboard}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1625,6 +1625,9 @@ export const getHostQueryPayload = (
|
||||
const diskPendingKey = dotMetricsEnabled
|
||||
? 'system.disk.pending_operations'
|
||||
: 'system_disk_pending_operations';
|
||||
const fsUsageKey = dotMetricsEnabled
|
||||
? 'system.filesystem.usage'
|
||||
: 'system_filesystem_usage';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -2657,6 +2660,155 @@ export const getHostQueryPayload = (
|
||||
start,
|
||||
end,
|
||||
},
|
||||
{
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
query: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'system_filesystem_usage--float64--Gauge--true',
|
||||
|
||||
key: fsUsageKey,
|
||||
type: 'Gauge',
|
||||
},
|
||||
aggregateOperator: 'avg',
|
||||
dataSource: DataSource.METRICS,
|
||||
disabled: true,
|
||||
expression: 'A',
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'fs_f1',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'host_name--string--tag--false',
|
||||
|
||||
key: hostNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: hostName,
|
||||
},
|
||||
{
|
||||
id: 'fs_f2',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'state--string--tag--false',
|
||||
|
||||
key: 'state',
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: 'used',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'mountpoint--string--tag--false',
|
||||
|
||||
key: 'mountpoint',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${fsUsageKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
legend: '{{mountpoint}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'avg',
|
||||
},
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'system_filesystem_usage--float64--Gauge--true',
|
||||
|
||||
key: fsUsageKey,
|
||||
type: 'Gauge',
|
||||
},
|
||||
aggregateOperator: 'avg',
|
||||
dataSource: DataSource.METRICS,
|
||||
disabled: true,
|
||||
expression: 'B',
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'fs_f3',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'host_name--string--tag--false',
|
||||
|
||||
key: hostNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: hostName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'mountpoint--string--tag--false',
|
||||
|
||||
key: 'mountpoint',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${fsUsageKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
legend: '{{mountpoint}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'B',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'avg',
|
||||
},
|
||||
],
|
||||
queryFormulas: [
|
||||
{
|
||||
disabled: false,
|
||||
expression: 'A/B',
|
||||
legend: '{{mountpoint}}',
|
||||
queryName: 'F1',
|
||||
},
|
||||
],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
promql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
},
|
||||
variables: {},
|
||||
formatForWeb: false,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -2732,4 +2884,5 @@ export const hostWidgetInfo = [
|
||||
{ title: 'System disk operations/s', yAxisUnit: 'short' },
|
||||
{ title: 'Queue size', yAxisUnit: 'short' },
|
||||
{ title: 'System disk operation time/s', yAxisUnit: 's' },
|
||||
{ title: 'Disk Usage (%) by mountpoint', yAxisUnit: 'percentunit' },
|
||||
];
|
||||
|
||||
@@ -164,7 +164,7 @@ function BreakDown(): JSX.Element {
|
||||
Meter metrics data is aggregated over 1 hour period. Please select time
|
||||
range accordingly.
|
||||
<a
|
||||
href="https://signoz.io/docs/cost-meter/overview/#accessing-cost-meter"
|
||||
href="https://signoz.io/docs/cost-meter/overview/#get-started"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
|
||||
@@ -3,12 +3,23 @@
|
||||
flex-direction: row;
|
||||
|
||||
.meter-explorer-quick-filters-section {
|
||||
width: 280px;
|
||||
// Width is owned by ResizableBox (inline style).
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.resizable-box__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.quick-filters-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.meter-explorer-content-section {
|
||||
@@ -78,7 +89,9 @@
|
||||
|
||||
&.quick-filters-open {
|
||||
.meter-explorer-content-section {
|
||||
width: calc(100% - 280px);
|
||||
flex: 1;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import cx from 'classnames';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
@@ -18,6 +19,8 @@ import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { Filter } from '@signozhq/icons';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ResizableBox } from 'periscope/components/ResizableBox';
|
||||
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -30,6 +33,10 @@ import { splitQueryIntoOneChartPerQuery } from './utils';
|
||||
|
||||
import './Explorer.styles.scss';
|
||||
|
||||
const QUICK_FILTERS_DEFAULT_WIDTH = 280;
|
||||
const QUICK_FILTERS_MIN_WIDTH = 240;
|
||||
const QUICK_FILTERS_MAX_WIDTH = 500;
|
||||
|
||||
function Explorer(): JSX.Element {
|
||||
const {
|
||||
handleRunQuery,
|
||||
@@ -55,6 +62,16 @@ function Explorer(): JSX.Element {
|
||||
|
||||
const [showQuickFilters, setShowQuickFilters] = useState(true);
|
||||
|
||||
const {
|
||||
initialWidth: quickFiltersInitialWidth,
|
||||
persistWidth: persistQuickFiltersWidth,
|
||||
} = usePanelWidth({
|
||||
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_METER,
|
||||
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
|
||||
minWidth: QUICK_FILTERS_MIN_WIDTH,
|
||||
maxWidth: QUICK_FILTERS_MAX_WIDTH,
|
||||
});
|
||||
|
||||
const defaultQuery = useMemo(
|
||||
() =>
|
||||
updateAllQueriesOperators(
|
||||
@@ -127,10 +144,19 @@ function Explorer(): JSX.Element {
|
||||
'quick-filters-open': showQuickFilters,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
|
||||
initialWidth={quickFiltersInitialWidth}
|
||||
minWidth={QUICK_FILTERS_MIN_WIDTH}
|
||||
maxWidth={QUICK_FILTERS_MAX_WIDTH}
|
||||
onResize={persistQuickFiltersWidth}
|
||||
resetToDefaultOnDoubleClick
|
||||
withHandle
|
||||
className={cx('meter-explorer-quick-filters-section', {
|
||||
hidden: !showQuickFilters,
|
||||
})}
|
||||
handleTestId="quick-filters-resize-handle"
|
||||
>
|
||||
<QuickFilters
|
||||
className="qf-meter-explorer"
|
||||
@@ -142,7 +168,7 @@ function Explorer(): JSX.Element {
|
||||
setShowQuickFilters(!showQuickFilters);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ResizableBox>
|
||||
|
||||
<div className="meter-explorer-content-section">
|
||||
<div className="meter-explorer-explore-content">
|
||||
|
||||
@@ -197,7 +197,7 @@ function TopOperationsTable({
|
||||
|
||||
const entryPointSpanInfo = {
|
||||
text: 'Shows the spans where requests enter new services for the first time',
|
||||
url: 'https://signoz.io/docs/traces-management/guides/entry-point-spans-service-overview/',
|
||||
url: 'https://signoz.io/docs/apm-and-distributed-tracing/application-details/',
|
||||
urlText: 'Learn more about Entrypoint Spans.',
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import azureOpenaiUrl from '@/assets/Logos/azure-openai.svg';
|
||||
import azureSqlDatabaseMetricsUrl from '@/assets/Logos/azure-sql-database-metrics.svg';
|
||||
import azureVmUrl from '@/assets/Logos/azure-vm.svg';
|
||||
import basetenUrl from '@/assets/Logos/baseten.svg';
|
||||
import cassandraUrl from '@/assets/Logos/cassandra.svg';
|
||||
import celeryUrl from '@/assets/Logos/celery.svg';
|
||||
import certManagerUrl from '@/assets/Logos/cert-manager.svg';
|
||||
import claudeCodeUrl from '@/assets/Logos/claude-code.svg';
|
||||
@@ -51,6 +52,7 @@ import externalApiMonitoringUrl from '@/assets/Logos/external-api-monitoring.svg
|
||||
import fluentbitUrl from '@/assets/Logos/fluentbit.svg';
|
||||
import fluentdUrl from '@/assets/Logos/fluentd.svg';
|
||||
import flutterMonitoringUrl from '@/assets/Logos/flutter-monitoring.svg';
|
||||
import fluxcdUrl from '@/assets/Logos/fluxcd.svg';
|
||||
import flyIoUrl from '@/assets/Logos/fly-io.svg';
|
||||
import fromLogFileUrl from '@/assets/Logos/from-log-file.svg';
|
||||
import gcpAppEngineUrl from '@/assets/Logos/gcp-app-engine.svg';
|
||||
@@ -94,6 +96,7 @@ import langtraceUrl from '@/assets/Logos/langtrace.svg';
|
||||
import litellmUrl from '@/assets/Logos/litellm.svg';
|
||||
import livekitUrl from '@/assets/Logos/livekit.svg';
|
||||
import llamaindexUrl from '@/assets/Logos/llamaindex.svg';
|
||||
import llmMonitoringUrl from '@/assets/Logos/llm-monitoring.svg';
|
||||
import logrusUrl from '@/assets/Logos/logrus.svg';
|
||||
import logsUrl from '@/assets/Logos/logs.svg';
|
||||
import logstashUrl from '@/assets/Logos/logstash.svg';
|
||||
@@ -120,6 +123,7 @@ import opentelemetryUrl from '@/assets/Logos/opentelemetry.svg';
|
||||
import phpUrl from '@/assets/Logos/php.svg';
|
||||
import pinoUrl from '@/assets/Logos/pino.svg';
|
||||
import pipecatUrl from '@/assets/Logos/pipecat.svg';
|
||||
import planetscaleUrl from '@/assets/Logos/planetscale.svg';
|
||||
import postgresqlUrl from '@/assets/Logos/postgresql.svg';
|
||||
import prometheusUrl from '@/assets/Logos/prometheus.svg';
|
||||
import pydanticAiUrl from '@/assets/Logos/pydantic-ai.svg';
|
||||
@@ -1526,6 +1530,28 @@ const onboardingConfigWithLinks = [
|
||||
id: 'nginx-tracing',
|
||||
link: '/docs/instrumentation/opentelemetry-nginx/',
|
||||
},
|
||||
{
|
||||
dataSource: 'nginx-ingress-controller',
|
||||
label: 'NGINX Ingress Controller',
|
||||
imgUrl: nginxUrl,
|
||||
tags: ['infrastructure monitoring'],
|
||||
module: 'metrics',
|
||||
relatedSearchKeywords: [
|
||||
'ingress',
|
||||
'ingress controller',
|
||||
'kubernetes ingress',
|
||||
'monitoring',
|
||||
'nginx ingress',
|
||||
'nginx ingress controller',
|
||||
'nginx ingress metrics',
|
||||
'nginx ingress monitoring',
|
||||
'nginx ingress observability',
|
||||
'observability',
|
||||
'opentelemetry nginx ingress',
|
||||
],
|
||||
id: 'nginx-ingress-controller',
|
||||
link: '/docs/metrics-management/nginx-ingress-controller/',
|
||||
},
|
||||
{
|
||||
dataSource: 'opentelemetry-cloudflare',
|
||||
label: 'Cloudflare Tracing',
|
||||
@@ -1586,6 +1612,119 @@ const onboardingConfigWithLinks = [
|
||||
id: 'opentelemetry-cloudflare-logs',
|
||||
link: '/docs/logs-management/send-logs/cloudflare-logs/',
|
||||
},
|
||||
{
|
||||
dataSource: 'cloudflare-workers',
|
||||
label: 'Cloudflare Workers',
|
||||
imgUrl: cloudflareUrl,
|
||||
tags: ['apm/traces'],
|
||||
module: 'apm',
|
||||
relatedSearchKeywords: [
|
||||
'cloudflare',
|
||||
'cloudflare workers',
|
||||
'cloudflare workers monitoring',
|
||||
'cloudflare workers observability',
|
||||
'cloudflare workers otlp',
|
||||
'edge computing monitoring',
|
||||
'monitor cloudflare workers',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'opentelemetry cloudflare workers',
|
||||
'otlp',
|
||||
'serverless monitoring',
|
||||
],
|
||||
id: 'cloudflare-workers',
|
||||
link: '/docs/integrations/outposts/cloudflare-workers/',
|
||||
},
|
||||
{
|
||||
dataSource: 'opentelemetry-cassandra',
|
||||
label: 'Cassandra',
|
||||
imgUrl: cassandraUrl,
|
||||
tags: ['database'],
|
||||
module: 'apm',
|
||||
relatedSearchKeywords: [
|
||||
'apache cassandra',
|
||||
'cassandra',
|
||||
'cassandra database',
|
||||
'cassandra logs',
|
||||
'cassandra metrics',
|
||||
'cassandra monitoring',
|
||||
'cassandra observability',
|
||||
'database',
|
||||
'monitoring',
|
||||
'nosql',
|
||||
'observability',
|
||||
'opentelemetry cassandra',
|
||||
],
|
||||
id: 'opentelemetry-cassandra',
|
||||
link: '/docs/integrations/opentelemetry-cassandra/',
|
||||
},
|
||||
{
|
||||
dataSource: 'fluxcd',
|
||||
label: 'FluxCD',
|
||||
imgUrl: fluxcdUrl,
|
||||
tags: ['infrastructure monitoring'],
|
||||
module: 'metrics',
|
||||
relatedSearchKeywords: [
|
||||
'continuous delivery',
|
||||
'flux',
|
||||
'fluxcd',
|
||||
'fluxcd dashboard',
|
||||
'fluxcd metrics',
|
||||
'fluxcd monitoring',
|
||||
'fluxcd observability',
|
||||
'gitops',
|
||||
'kubernetes',
|
||||
'monitoring',
|
||||
'observability',
|
||||
'opentelemetry fluxcd',
|
||||
],
|
||||
id: 'fluxcd',
|
||||
link: '/docs/metrics-management/fluxcd-metrics/',
|
||||
},
|
||||
{
|
||||
dataSource: 'planetscale',
|
||||
label: 'PlanetScale',
|
||||
imgUrl: planetscaleUrl,
|
||||
tags: ['database'],
|
||||
module: 'apm',
|
||||
relatedSearchKeywords: [
|
||||
'database',
|
||||
'monitoring',
|
||||
'mysql',
|
||||
'observability',
|
||||
'opentelemetry planetscale',
|
||||
'planetscale',
|
||||
'planetscale database',
|
||||
'planetscale metrics',
|
||||
'planetscale monitoring',
|
||||
'planetscale observability',
|
||||
'serverless database',
|
||||
],
|
||||
id: 'planetscale',
|
||||
link: '/docs/metrics-management/opentelemetry-planetscale/',
|
||||
},
|
||||
{
|
||||
dataSource: 'hermes-agent',
|
||||
label: 'Hermes Agent',
|
||||
imgUrl: llmMonitoringUrl,
|
||||
tags: ['LLM Monitoring'],
|
||||
module: 'apm',
|
||||
relatedSearchKeywords: [
|
||||
'ai agent monitoring',
|
||||
'hermes',
|
||||
'hermes agent',
|
||||
'hermes agent monitoring',
|
||||
'hermes agent observability',
|
||||
'hermes monitoring',
|
||||
'llm monitoring',
|
||||
'monitoring',
|
||||
'nous research',
|
||||
'observability',
|
||||
'opentelemetry hermes',
|
||||
],
|
||||
id: 'hermes-agent',
|
||||
link: '/docs/hermes-monitoring/',
|
||||
},
|
||||
{
|
||||
dataSource: 'convex-logs',
|
||||
label: 'Convex Logs',
|
||||
|
||||
@@ -64,7 +64,7 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
Enter OAuth 2.0 credentials obtained from the Google API Console below.
|
||||
Read the{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/userguide/sso-authentication"
|
||||
href="https://signoz.io/docs/manage/administrator-guide/sso/overview/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
||||
@@ -38,7 +38,7 @@ function ConfigureOIDCAuthnProvider({
|
||||
Configure OpenID Connect Single Sign-On with your Identity Provider. Read
|
||||
the{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/userguide/sso-authentication"
|
||||
href="https://signoz.io/docs/manage/administrator-guide/sso/overview/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
||||
@@ -37,7 +37,7 @@ function ConfigureSAMLAuthnProvider({
|
||||
<p className="authn-provider__description">
|
||||
Configure SAML 2.0 Single Sign-On with your Identity Provider. Read the{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/userguide/sso-authentication"
|
||||
href="https://signoz.io/docs/manage/administrator-guide/sso/overview/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
||||
@@ -216,7 +216,7 @@ export default function QueryFunctions({
|
||||
Add new function
|
||||
<Typography.Link
|
||||
style={{ textDecoration: 'underline' }}
|
||||
href="https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=query-builder#functions-for-extended-data-analysis"
|
||||
href="https://signoz.io/docs/querying/functions-extended-analysis/?utm_source=product&utm_medium=query-builder"
|
||||
target="_blank"
|
||||
>
|
||||
{' '}
|
||||
|
||||
@@ -48,6 +48,8 @@ describe('CreateRolePage - AuthZ', () => {
|
||||
isFetching: true,
|
||||
error: null,
|
||||
permissions: null,
|
||||
allowed: false,
|
||||
deniedPermissions: [],
|
||||
refetchPermissions: jest.fn(),
|
||||
});
|
||||
|
||||
|
||||
@@ -77,6 +77,8 @@ describe('EditRolePage - AuthZ', () => {
|
||||
isFetching: true,
|
||||
error: null,
|
||||
permissions: null,
|
||||
allowed: false,
|
||||
deniedPermissions: [],
|
||||
refetchPermissions: jest.fn(),
|
||||
});
|
||||
|
||||
|
||||
@@ -409,6 +409,8 @@ describe('ViewRolePage - AuthZ', () => {
|
||||
isFetching: true,
|
||||
error: null,
|
||||
permissions: null,
|
||||
allowed: false,
|
||||
deniedPermissions: [],
|
||||
refetchPermissions: jest.fn(),
|
||||
});
|
||||
|
||||
|
||||
@@ -203,8 +203,8 @@ export const routesToSkip = [
|
||||
ROUTES.METER_EXPLORER_VIEWS,
|
||||
ROUTES.METRICS_EXPLORER_VOLUME_CONTROL,
|
||||
ROUTES.SOMETHING_WENT_WRONG,
|
||||
ROUTES.LLM_OBSERVABILITY_BASE,
|
||||
ROUTES.LLM_OBSERVABILITY_MODEL_PRICING,
|
||||
ROUTES.LLM_OBSERVABILITY_OVERVIEW,
|
||||
ROUTES.LLM_OBSERVABILITY_CONFIGURATION,
|
||||
];
|
||||
|
||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user