Compare commits

..

11 Commits

Author SHA1 Message Date
Nikhil Mantri
d3191d4a67 Merge branch 'main' into infraM/remove_missing_metrics_checks 2026-06-22 12:35:15 +05:30
nikhilmantri0902
3d6f164dc1 chore: removed unwanted provisional comments 2026-06-19 14:52:50 +05:30
Nikhil Mantri
c15ac1294c Merge branch 'main' into infraM/remove_missing_metrics_checks 2026-06-19 14:13:25 +05:30
nikhilmantri0902
8613c4e053 chore: updated integration tests for jobs and daemonsets 2026-06-19 13:56:49 +05:30
nikhilmantri0902
5d332ef02b chore: updated statefulsets integration tests 2026-06-19 13:34:25 +05:30
nikhilmantri0902
b1e6380d30 chore: updated integration tests for clusters and deployments 2026-06-19 13:22:53 +05:30
nikhilmantri0902
f47f5693fd chore: updated integration tests for nodes 2026-06-19 12:17:59 +05:30
nikhilmantri0902
12f8f9bffe chore: updated integration tests for volumes 2026-06-19 12:08:51 +05:30
nikhilmantri0902
32dacb7a2a chore: updated integration tests for pods 2026-06-19 11:55:17 +05:30
nikhilmantri0902
885e29cd7b refactor(inframonitoring): drop required-metrics test, add test hosts_warnings 2026-06-19 03:04:47 +05:30
nikhilmantri0902
f182ee0c49 chore: metric_name required checks removed 2026-06-18 21:40:36 +05:30
143 changed files with 2837 additions and 6622 deletions

69
.github/CODEOWNERS vendored
View File

@@ -199,72 +199,3 @@ go.mod @therealpandey
## OpenAPI Schema - Generated
/frontend/src/api/generated/services/ @therealpandey @vikrantgupta25 @srikanthccv
/docs/api/openapi.yml @therealpandey @vikrantgupta25 @srikanthccv
## Logs
/frontend/src/pages/Logs/ @SigNoz/events-frontend
/frontend/src/pages/LogsExplorer/ @SigNoz/events-frontend
/frontend/src/pages/LogsModulePage/ @SigNoz/events-frontend
/frontend/src/pages/LogsSettings/ @SigNoz/events-frontend
/frontend/src/pages/LiveLogs/ @SigNoz/events-frontend
/frontend/src/container/LogsExplorerChart/ @SigNoz/events-frontend
/frontend/src/container/LogsExplorerContext/ @SigNoz/events-frontend
/frontend/src/container/LogsExplorerList/ @SigNoz/events-frontend
/frontend/src/container/LogsExplorerTable/ @SigNoz/events-frontend
/frontend/src/container/LogsExplorerViews/ @SigNoz/events-frontend
/frontend/src/container/LogsFilters/ @SigNoz/events-frontend
/frontend/src/container/LogsSearchFilter/ @SigNoz/events-frontend
/frontend/src/container/LogsTable/ @SigNoz/events-frontend
/frontend/src/container/LogsAggregate/ @SigNoz/events-frontend
/frontend/src/container/LogsContextList/ @SigNoz/events-frontend
/frontend/src/container/LogsIndexToFields/ @SigNoz/events-frontend
/frontend/src/container/LogsLoading/ @SigNoz/events-frontend
/frontend/src/container/LogsPanelTable/ @SigNoz/events-frontend
/frontend/src/container/LogControls/ @SigNoz/events-frontend
/frontend/src/container/LogDetailedView/ @SigNoz/events-frontend
/frontend/src/container/LogExplorerQuerySection/ @SigNoz/events-frontend
/frontend/src/container/LogLiveTail/ @SigNoz/events-frontend
/frontend/src/container/LiveLogs/ @SigNoz/events-frontend
/frontend/src/container/EmptyLogsSearch/ @SigNoz/events-frontend
/frontend/src/container/NoLogs/ @SigNoz/events-frontend
/frontend/src/components/Logs/ @SigNoz/events-frontend
/frontend/src/components/LogDetail/ @SigNoz/events-frontend
/frontend/src/components/LogsFormatOptionsMenu/ @SigNoz/events-frontend
/frontend/src/hooks/logs/ @SigNoz/events-frontend
## Logs Pipelines
/frontend/src/pages/Pipelines/ @SigNoz/events-frontend
/frontend/src/container/PipelinePage/ @SigNoz/events-frontend
## Traces / Trace Explorer
/frontend/src/pages/Trace/ @SigNoz/events-frontend
/frontend/src/pages/TracesExplorer/ @SigNoz/events-frontend
/frontend/src/pages/TracesModulePage/ @SigNoz/events-frontend
/frontend/src/container/Trace/ @SigNoz/events-frontend
/frontend/src/container/TracesExplorer/ @SigNoz/events-frontend
/frontend/src/container/TracesTableComponent/ @SigNoz/events-frontend
## Trace Funnels
/frontend/src/pages/TracesFunnels/ @SigNoz/events-frontend
/frontend/src/pages/TracesFunnelDetails/ @SigNoz/events-frontend
/frontend/src/hooks/TracesFunnels/ @SigNoz/events-frontend
## Trace Details
/frontend/src/pages/TraceDetailsV3/ @SigNoz/events-frontend
/frontend/src/pages/TraceDetailOldRedirect/ @SigNoz/events-frontend
/frontend/src/hooks/trace/ @SigNoz/events-frontend
## Exceptions
/frontend/src/pages/AllErrors/ @SigNoz/events-frontend
/frontend/src/pages/ErrorDetails/ @SigNoz/events-frontend
/frontend/src/container/AllError/ @SigNoz/events-frontend
/frontend/src/container/ErrorDetails/ @SigNoz/events-frontend
## External APIs
/frontend/src/pages/ApiMonitoring/ @SigNoz/events-frontend
/frontend/src/container/ApiMonitoring/ @SigNoz/events-frontend
## Messaging Queues
/frontend/src/pages/MessagingQueues/ @SigNoz/events-frontend
/frontend/src/components/MessagingQueues/ @SigNoz/events-frontend
/frontend/src/components/MessagingQueueHealthCheck/ @SigNoz/events-frontend
/frontend/src/hooks/messagingQueue/ @SigNoz/events-frontend

View File

@@ -647,41 +647,14 @@ components:
type: string
name:
type: string
transactionGroups:
$ref: '#/components/schemas/AuthtypesTransactionGroups'
required:
- name
- description
- transactionGroups
type: object
AuthtypesPostableRotateToken:
properties:
refreshToken:
type: string
type: object
AuthtypesPostableUser:
properties:
displayName:
type: string
email:
type: string
frontendBaseUrl:
type: string
userRoles:
items:
$ref: '#/components/schemas/AuthtypesPostableUserRole'
type: array
required:
- email
- userRoles
type: object
AuthtypesPostableUserRole:
properties:
id:
type: string
required:
- id
type: object
AuthtypesRelation:
enum:
- create
@@ -730,34 +703,6 @@ components:
useRoleAttribute:
type: boolean
type: object
AuthtypesRoleWithTransactionGroups:
properties:
createdAt:
format: date-time
type: string
description:
type: string
id:
type: string
name:
type: string
orgId:
type: string
transactionGroups:
$ref: '#/components/schemas/AuthtypesTransactionGroups'
type:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- description
- type
- orgId
- transactionGroups
type: object
AuthtypesSamlConfig:
properties:
attributeMapping:
@@ -791,35 +736,11 @@ components:
- relation
- object
type: object
AuthtypesTransactionGroup:
properties:
objectGroup:
$ref: '#/components/schemas/CoretypesObjectGroup'
relation:
$ref: '#/components/schemas/AuthtypesRelation'
required:
- relation
- objectGroup
type: object
AuthtypesTransactionGroups:
items:
$ref: '#/components/schemas/AuthtypesTransactionGroup'
type: array
AuthtypesUpdatableAuthDomain:
properties:
config:
$ref: '#/components/schemas/AuthtypesAuthDomainConfig'
type: object
AuthtypesUpdatableRole:
properties:
description:
type: string
transactionGroups:
$ref: '#/components/schemas/AuthtypesTransactionGroups'
required:
- description
- transactionGroups
type: object
AuthtypesUserRole:
properties:
createdAt:
@@ -4017,8 +3938,6 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesClusterRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4029,7 +3948,6 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesDaemonSetRecord:
@@ -4086,8 +4004,6 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesDaemonSetRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4098,7 +4014,6 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesDeploymentRecord:
@@ -4155,8 +4070,6 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesDeploymentRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4167,7 +4080,6 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesHostFilter:
@@ -4233,8 +4145,6 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesHostRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4245,7 +4155,6 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesJobRecord:
@@ -4308,8 +4217,6 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesJobRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4320,7 +4227,6 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesNamespaceRecord:
@@ -4355,8 +4261,6 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesNamespaceRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4367,7 +4271,6 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesNodeCondition:
@@ -4432,8 +4335,6 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesNodeRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4444,7 +4345,6 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesPodCountsByPhase:
@@ -4530,8 +4430,6 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesPodRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4542,7 +4440,6 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesPostableClusters:
@@ -4805,16 +4702,6 @@ components:
- end
- limit
type: object
InframonitoringtypesRequiredMetricsCheck:
properties:
missingMetrics:
items:
type: string
nullable: true
type: array
required:
- missingMetrics
type: object
InframonitoringtypesResponseType:
enum:
- list
@@ -4874,8 +4761,6 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesStatefulSetRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4886,7 +4771,6 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesVolumeRecord:
@@ -4934,8 +4818,6 @@ components:
items:
$ref: '#/components/schemas/InframonitoringtypesVolumeRecord'
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
@@ -4946,7 +4828,6 @@ components:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
LlmpricingruletypesGettablePricingRules:
@@ -10206,7 +10087,7 @@ paths:
- global
/api/v1/invite:
post:
deprecated: true
deprecated: false
description: This endpoint creates an invite for a user
operationId: CreateInvite
requestBody:
@@ -10269,7 +10150,7 @@ paths:
- users
/api/v1/invite/bulk:
post:
deprecated: true
deprecated: false
description: This endpoint creates a bulk invite for a user
operationId: CreateBulkInvite
requestBody:
@@ -10332,15 +10213,6 @@ paths:
name: limit
schema:
type: integer
- in: query
name: q
schema:
type: string
- in: query
name: isOverride
schema:
nullable: true
type: boolean
responses:
"200":
content:
@@ -11146,7 +11018,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/AuthtypesRoleWithTransactionGroups'
$ref: '#/components/schemas/AuthtypesRole'
status:
type: string
required:
@@ -11181,7 +11053,7 @@ paths:
tags:
- role
patch:
deprecated: true
deprecated: false
description: This endpoint patches a role
operationId: PatchRole
parameters:
@@ -11242,68 +11114,6 @@ paths:
summary: Patch role
tags:
- role
put:
deprecated: false
description: This endpoint updates a role
operationId: UpdateRole
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/AuthtypesUpdatableRole'
responses:
"204":
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"451":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unavailable For Legal Reasons
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
"501":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Implemented
security:
- api_key:
- role:update
- tokenizer:
- role:update
summary: Update role
tags:
- role
/api/v1/roles/{id}/relations/{relation}/objects:
get:
deprecated: false
@@ -11383,7 +11193,7 @@ paths:
tags:
- role
patch:
deprecated: true
deprecated: false
description: Patches the objects connected to the specified role via a given
relation type
operationId: PatchObjects
@@ -13110,7 +12920,7 @@ paths:
- tracedetail
/api/v1/user:
get:
deprecated: true
deprecated: false
description: This endpoint lists all users
operationId: ListUsersDeprecated
responses:
@@ -13203,7 +13013,7 @@ paths:
tags:
- users
get:
deprecated: true
deprecated: false
description: This endpoint returns the user by id
operationId: GetUserDeprecated
parameters:
@@ -13260,7 +13070,7 @@ paths:
tags:
- users
put:
deprecated: true
deprecated: false
description: This endpoint updates the user by id
operationId: UpdateUserDeprecated
parameters:
@@ -13329,7 +13139,7 @@ paths:
- users
/api/v1/user/me:
get:
deprecated: true
deprecated: false
description: This endpoint returns the user I belong to
operationId: GetMyUserDeprecated
responses:
@@ -14845,10 +14655,10 @@ paths:
for custom groupBy keys; in both modes every row aggregates nodes and pods
in the group. Supports filtering via a filter expression, custom groupBy,
ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination
via offset/limit. Also reports missing required metrics and whether the requested
time range falls before the data retention boundary. Numeric metric fields
(clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable)
return -1 as a sentinel when no data is available for that field.'
via offset/limit. Also reports whether the requested time range falls before
the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable,
clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data
is available for that field.'
operationId: ListClusters
requestBody:
content:
@@ -14921,11 +14731,11 @@ paths:
row aggregates pods owned by daemonsets in the group. Supports filtering via
a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit
/ memory / memory_request / memory_limit / desired_nodes / current_nodes,
and pagination via offset/limit. Also reports missing required metrics and
whether the requested time range falls before the data retention boundary.
Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit,
daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes,
currentNodes) return -1 as a sentinel when no data is available for that field.'
and pagination via offset/limit. Also reports whether the requested time range
falls before the data retention boundary. Numeric metric fields (daemonSetCPU,
daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest,
daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel
when no data is available for that field.'
operationId: ListDaemonSets
requestBody:
content:
@@ -14996,11 +14806,11 @@ paths:
group. Supports filtering via a filter expression, custom groupBy, ordering
by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit
/ desired_pods / available_pods, and pagination via offset/limit. Also reports
missing required metrics and whether the requested time range falls before
the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest,
deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit,
desiredPods, availablePods) return -1 as a sentinel when no data is available
for that field.'
whether the requested time range falls before the data retention boundary.
Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit,
deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods,
availablePods) return -1 as a sentinel when no data is available for that
field.'
operationId: ListDeployments
requestBody:
content:
@@ -15065,10 +14875,9 @@ paths:
custom groupBy to aggregate hosts by any attribute, ordering by any of the
five metrics, and pagination via offset/limit. The response type is ''list''
for the default host.name grouping or ''grouped_list'' for custom groupBy
keys. Also reports missing required metrics and whether the requested time
range falls before the data retention boundary. Numeric metric fields (cpu,
memory, wait, load15, diskUsage) return -1 as a sentinel when no data is available
for that field.'
keys. Also reports whether the requested time range falls before the data
retention boundary. Numeric metric fields (cpu, memory, wait, load15, diskUsage)
return -1 as a sentinel when no data is available for that field.'
operationId: ListHosts
requestBody:
content:
@@ -15142,11 +14951,11 @@ paths:
jobs in the group. Supports filtering via a filter expression, custom groupBy,
ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit
/ desired_successful_pods / active_pods / failed_pods / successful_pods, and
pagination via offset/limit. Also reports missing required metrics and whether
the requested time range falls before the data retention boundary. Numeric
metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest,
jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods)
return -1 as a sentinel when no data is available for that field.'
pagination via offset/limit. Also reports whether the requested time range
falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest,
jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods,
activePods, failedPods, successfulPods) return -1 as a sentinel when no data
is available for that field.'
operationId: ListJobs
requestBody:
content:
@@ -15211,10 +15020,10 @@ paths:
type is ''list'' for the default k8s.namespace.name grouping or ''grouped_list''
for custom groupBy keys; in both modes every row aggregates pods in the group.
Supports filtering via a filter expression, custom groupBy, ordering by cpu
/ memory, and pagination via offset/limit. Also reports missing required metrics
and whether the requested time range falls before the data retention boundary.
Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel
when no data is available for that field.'
/ memory, and pagination via offset/limit. Also reports whether the requested
time range falls before the data retention boundary. Numeric metric fields
(namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available
for that field.'
operationId: ListNamespaces
requestBody:
content:
@@ -15282,10 +15091,10 @@ paths:
for custom groupBy keys (each row aggregates nodes in the group; condition
stays no_data). Supports filtering via a filter expression, custom groupBy,
ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination
via offset/limit. Also reports missing required metrics and whether the requested
time range falls before the data retention boundary. Numeric metric fields
(nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1
as a sentinel when no data is available for that field.'
via offset/limit. Also reports whether the requested time range falls before
the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable,
nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is
available for that field.'
operationId: ListNodes
requestBody:
content:
@@ -15354,11 +15163,10 @@ paths:
is one pod with its current phase) or ''grouped_list'' for custom groupBy
keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase:
{ pending, running, succeeded, failed, unknown } derived from each pod''s
latest phase in the window). Also reports missing required metrics and whether
the requested time range falls before the data retention boundary. Numeric
metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest,
podMemoryLimit, podAge) return -1 as a sentinel when no data is available
for that field.'
latest phase in the window). Also reports whether the requested time range
falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest,
podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1
as a sentinel when no data is available for that field.'
operationId: ListPods
requestBody:
content:
@@ -15425,11 +15233,10 @@ paths:
usage, inodes, inodes_free, inodes_used), and pagination via offset/limit.
The response type is ''list'' for the default k8s.persistentvolumeclaim.name
grouping or ''grouped_list'' for custom groupBy keys; in both modes every
row aggregates volumes in the group. Also reports missing required metrics
and whether the requested time range falls before the data retention boundary.
Numeric metric fields (volumeAvailable, volumeCapacity, volumeUsage, volumeInodes,
volumeInodesFree, volumeInodesUsed) return -1 as a sentinel when no data is
available for that field.'
row aggregates volumes in the group. Also reports whether the requested time
range falls before the data retention boundary. Numeric metric fields (volumeAvailable,
volumeCapacity, volumeUsage, volumeInodes, volumeInodesFree, volumeInodesUsed)
return -1 as a sentinel when no data is available for that field.'
operationId: ListVolumes
requestBody:
content:
@@ -15500,11 +15307,10 @@ paths:
statefulsets in the group. Supports filtering via a filter expression, custom
groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request
/ memory_limit / desired_pods / current_pods, and pagination via offset/limit.
Also reports missing required metrics and whether the requested time range
falls before the data retention boundary. Numeric metric fields (statefulSetCPU,
statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest,
statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel
when no data is available for that field.'
Also reports whether the requested time range falls before the data retention
boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit,
statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods,
currentPods) return -1 as a sentinel when no data is available for that field.'
operationId: ListStatefulSets
requestBody:
content:
@@ -20745,68 +20551,6 @@ paths:
summary: List users v2
tags:
- users
post:
deprecated: false
description: This endpoint creates a user for the organization
operationId: CreateUser
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/AuthtypesPostableUser'
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
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Create user
tags:
- users
/api/v2/users/{id}:
get:
deprecated: false

View File

@@ -179,36 +179,13 @@ func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context,
return provider.Write(ctx, tuples, nil)
}
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.RoleWithTransactionGroups) error {
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
existingRole, err := provider.GetByOrgIDAndName(ctx, orgID, role.Name)
if err != nil && !errors.Asc(err, authtypes.ErrCodeRoleNotFound) {
return err
}
if existingRole != nil {
return errors.Newf(errors.TypeAlreadyExists, authtypes.ErrCodeRoleAlreadyExists, "role with name: %s already exists", existingRole.Name)
}
tuples, err := authtypes.NewTuplesFromTransactionGroups(role.Name, orgID, role.TransactionGroups)
if err != nil {
return err
}
err = provider.Write(ctx, tuples, nil)
if err != nil {
return err
}
if err := provider.store.Create(ctx, role.Role); err != nil {
return err
}
return nil
return provider.store.Create(ctx, role)
}
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) (*authtypes.Role, error) {
@@ -236,26 +213,6 @@ func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, ro
return role, nil
}
func (provider *provider) GetWithTransactionGroups(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.RoleWithTransactionGroups, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
role, err := provider.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
tuples, err := provider.readAllTuplesForRole(ctx, role.Name, orgID)
if err != nil {
return nil, err
}
transactionGroups := authtypes.MustNewTransactionGroupsFromTuples(tuples)
return authtypes.MakeRoleWithTransactionGroups(role, transactionGroups), nil
}
func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*coretypes.Object, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
@@ -290,36 +247,6 @@ func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id
return objects, nil
}
func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updatedRole *authtypes.RoleWithTransactionGroups) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
existingRole, err := provider.GetWithTransactionGroups(ctx, orgID, updatedRole.ID)
if err != nil {
return err
}
additions, deletions := existingRole.TransactionGroups.Diff(updatedRole.TransactionGroups)
additionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, additions)
if err != nil {
return err
}
deletionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, deletions)
if err != nil {
return err
}
err = provider.Write(ctx, additionTuples, deletionTuples)
if err != nil {
return err
}
return provider.store.Update(ctx, orgID, updatedRole.Role)
}
func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
@@ -359,7 +286,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
role, err := provider.GetWithTransactionGroups(ctx, orgID, id)
role, err := provider.store.Get(ctx, orgID, id)
if err != nil {
return err
}
@@ -375,12 +302,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
}
}
tuples, err := authtypes.NewTuplesFromTransactionGroups(role.Name, orgID, role.TransactionGroups)
if err != nil {
return err
}
if err := provider.Write(ctx, nil, tuples); err != nil {
if err := provider.deleteTuples(ctx, role.Name, orgID); err != nil {
return errors.WithAdditionalf(err, "failed to delete tuples for the role: %s", role.Name)
}
@@ -439,7 +361,7 @@ func (provider *provider) getManagedRoleTransactionTuples(orgID valuer.UUID) []*
return tuples
}
func (provider *provider) readAllTuplesForRole(ctx context.Context, roleName string, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
func (provider *provider) deleteTuples(ctx context.Context, roleName string, orgID valuer.UUID) error {
subject := authtypes.MustNewSubject(coretypes.NewResourceRole(), roleName, orgID, &coretypes.VerbAssignee)
tuples := make([]*openfgav1.TupleKey, 0)
@@ -449,10 +371,26 @@ func (provider *provider) readAllTuplesForRole(ctx context.Context, roleName str
Object: objectType.StringValue() + ":",
})
if err != nil {
return nil, err
return err
}
tuples = append(tuples, typeTuples...)
}
return tuples, nil
if len(tuples) == 0 {
return nil
}
for idx := 0; idx < len(tuples); idx += provider.config.OpenFGA.MaxTuplesPerWrite {
end := idx + provider.config.OpenFGA.MaxTuplesPerWrite
if end > len(tuples) {
end = len(tuples)
}
err := provider.Write(ctx, nil, tuples[idx:end])
if err != nil {
return err
}
}
return nil
}

View File

@@ -98,15 +98,6 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
Route: "",
})
aiObservability := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureEnableAIObservability, evalCtx)
featureSet = append(featureSet, &licensetypes.Feature{
Name: valuer.NewString(flagger.FeatureEnableAIObservability.String()),
Active: aiObservability,
Usage: 0,
UsageLimit: -1,
Route: "",
})
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {

View File

@@ -39,7 +39,7 @@ import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* @summary List Clusters for Infra Monitoring
*/
export const listClusters = (
@@ -122,7 +122,7 @@ export const useListClusters = <
return useMutation(getListClustersMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel when no data is available for that field.
* @summary List DaemonSets for Infra Monitoring
*/
export const listDaemonSets = (
@@ -205,7 +205,7 @@ export const useListDaemonSets = <
return useMutation(getListDaemonSetsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes Deployments with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest, deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each row also reports the latest known desiredPods (k8s.deployment.desired) and availablePods (k8s.deployment.available) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each deployment includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.deployment.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by deployments in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / available_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods, availablePods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes Deployments with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest, deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each row also reports the latest known desiredPods (k8s.deployment.desired) and availablePods (k8s.deployment.available) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each deployment includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.deployment.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by deployments in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / available_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods, availablePods) return -1 as a sentinel when no data is available for that field.
* @summary List Deployments for Infra Monitoring
*/
export const listDeployments = (
@@ -288,7 +288,7 @@ export const useListDeployments = <
return useMutation(getListDeploymentsMutationOptions(options));
};
/**
* Returns a paginated list of hosts with key infrastructure metrics: CPU usage (%), memory usage (%), I/O wait (%), disk usage (%), and 15-minute load average. Each host includes its current status (active/inactive based on metrics reported in the last 10 minutes) and metadata attributes (e.g., os.type). Supports filtering via a filter expression, filtering by host status, custom groupBy to aggregate hosts by any attribute, ordering by any of the five metrics, and pagination via offset/limit. The response type is 'list' for the default host.name grouping or 'grouped_list' for custom groupBy keys. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (cpu, memory, wait, load15, diskUsage) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of hosts with key infrastructure metrics: CPU usage (%), memory usage (%), I/O wait (%), disk usage (%), and 15-minute load average. Each host includes its current status (active/inactive based on metrics reported in the last 10 minutes) and metadata attributes (e.g., os.type). Supports filtering via a filter expression, filtering by host status, custom groupBy to aggregate hosts by any attribute, ordering by any of the five metrics, and pagination via offset/limit. The response type is 'list' for the default host.name grouping or 'grouped_list' for custom groupBy keys. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (cpu, memory, wait, load15, diskUsage) return -1 as a sentinel when no data is available for that field.
* @summary List Hosts for Infra Monitoring
*/
export const listHosts = (
@@ -371,7 +371,7 @@ export const useListHosts = <
return useMutation(getListHostsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value); note podCountsByPhase.failed (current pod-phase) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value); note podCountsByPhase.failed (current pod-phase) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.
* @summary List Jobs for Infra Monitoring
*/
export const listJobs = (
@@ -454,7 +454,7 @@ export const useListJobs = <
return useMutation(getListJobsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.
* @summary List Namespaces for Infra Monitoring
*/
export const listNamespaces = (
@@ -537,7 +537,7 @@ export const useListNamespaces = <
return useMutation(getListNamespacesMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } for pods scheduled on the listed nodes). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } for pods scheduled on the listed nodes). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* @summary List Nodes for Infra Monitoring
*/
export const listNodes = (
@@ -620,7 +620,7 @@ export const useListNodes = <
return useMutation(getListNodesMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed/unknown/no_data), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current phase) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase: { pending, running, succeeded, failed, unknown } derived from each pod's latest phase in the window). Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed/unknown/no_data), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current phase) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase: { pending, running, succeeded, failed, unknown } derived from each pod's latest phase in the window). Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.
* @summary List Pods for Infra Monitoring
*/
export const listPods = (
@@ -703,7 +703,7 @@ export const useListPods = <
return useMutation(getListPodsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes persistent volume claims (PVCs) with key volume metrics: available bytes, capacity bytes, usage (capacity - available), inodes, free inodes, and used inodes. Each row also includes metadata attributes (k8s.persistentvolumeclaim.name, k8s.pod.uid, k8s.pod.name, k8s.namespace.name, k8s.node.name, k8s.statefulset.name, k8s.cluster.name). Supports filtering via a filter expression, custom groupBy to aggregate volumes by any attribute, ordering by any of the six metrics (available, capacity, usage, inodes, inodes_free, inodes_used), and pagination via offset/limit. The response type is 'list' for the default k8s.persistentvolumeclaim.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates volumes in the group. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (volumeAvailable, volumeCapacity, volumeUsage, volumeInodes, volumeInodesFree, volumeInodesUsed) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes persistent volume claims (PVCs) with key volume metrics: available bytes, capacity bytes, usage (capacity - available), inodes, free inodes, and used inodes. Each row also includes metadata attributes (k8s.persistentvolumeclaim.name, k8s.pod.uid, k8s.pod.name, k8s.namespace.name, k8s.node.name, k8s.statefulset.name, k8s.cluster.name). Supports filtering via a filter expression, custom groupBy to aggregate volumes by any attribute, ordering by any of the six metrics (available, capacity, usage, inodes, inodes_free, inodes_used), and pagination via offset/limit. The response type is 'list' for the default k8s.persistentvolumeclaim.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates volumes in the group. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (volumeAvailable, volumeCapacity, volumeUsage, volumeInodes, volumeInodesFree, volumeInodesUsed) return -1 as a sentinel when no data is available for that field.
* @summary List Volumes for Infra Monitoring
*/
export const listVolumes = (
@@ -786,7 +786,7 @@ export const useListVolumes = <
return useMutation(getListVolumesMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.
* @summary List StatefulSets for Infra Monitoring
*/
export const listStatefulSets = (

View File

@@ -20,7 +20,6 @@ import type {
import type {
AuthtypesPatchableRoleDTO,
AuthtypesPostableRoleDTO,
AuthtypesUpdatableRoleDTO,
CoretypesPatchableObjectsDTO,
CreateRole201,
DeleteRolePathParameters,
@@ -32,7 +31,6 @@ import type {
PatchObjectsPathParameters,
PatchRolePathParameters,
RenderErrorResponseDTO,
UpdateRolePathParameters,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
@@ -367,7 +365,6 @@ export const invalidateGetRole = async (
/**
* This endpoint patches a role
* @deprecated
* @summary Patch role
*/
export const patchRole = (
@@ -439,7 +436,6 @@ export type PatchRoleMutationBody =
export type PatchRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Patch role
*/
export const usePatchRole = <
@@ -466,105 +462,6 @@ export const usePatchRole = <
> => {
return useMutation(getPatchRoleMutationOptions(options));
};
/**
* This endpoint updates a role
* @summary Update role
*/
export const updateRole = (
{ id }: UpdateRolePathParameters,
authtypesUpdatableRoleDTO?: BodyType<AuthtypesUpdatableRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/roles/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: authtypesUpdatableRoleDTO,
signal,
});
};
export const getUpdateRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateRole>>,
TError,
{
pathParams: UpdateRolePathParameters;
data?: BodyType<AuthtypesUpdatableRoleDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateRole>>,
TError,
{
pathParams: UpdateRolePathParameters;
data?: BodyType<AuthtypesUpdatableRoleDTO>;
},
TContext
> => {
const mutationKey = ['updateRole'];
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 updateRole>>,
{
pathParams: UpdateRolePathParameters;
data?: BodyType<AuthtypesUpdatableRoleDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateRole(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof updateRole>>
>;
export type UpdateRoleMutationBody =
| BodyType<AuthtypesUpdatableRoleDTO>
| undefined;
export type UpdateRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update role
*/
export const useUpdateRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateRole>>,
TError,
{
pathParams: UpdateRolePathParameters;
data?: BodyType<AuthtypesUpdatableRoleDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateRole>>,
TError,
{
pathParams: UpdateRolePathParameters;
data?: BodyType<AuthtypesUpdatableRoleDTO>;
},
TContext
> => {
return useMutation(getUpdateRoleMutationOptions(options));
};
/**
* Gets all objects connected to the specified role via a given relation type
* @summary Get objects for a role by relation
@@ -668,7 +565,6 @@ export const invalidateGetObjects = async (
/**
* Patches the objects connected to the specified role via a given relation type
* @deprecated
* @summary Patch objects for a role by relation
*/
export const patchObjects = (
@@ -740,7 +636,6 @@ export type PatchObjectsMutationBody =
export type PatchObjectsMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Patch objects for a role by relation
*/
export const usePatchObjects = <

View File

@@ -2224,31 +2224,15 @@ export interface AuthtypesPostableEmailPasswordSessionDTO {
password?: string;
}
export interface CoretypesObjectGroupDTO {
resource: CoretypesResourceRefDTO;
/**
* @type array
*/
selectors: string[];
}
export interface AuthtypesTransactionGroupDTO {
objectGroup: CoretypesObjectGroupDTO;
relation: AuthtypesRelationDTO;
}
export type AuthtypesTransactionGroupsDTO = AuthtypesTransactionGroupDTO[];
export interface AuthtypesPostableRoleDTO {
/**
* @type string
*/
description: string;
description?: string;
/**
* @type string
*/
name: string;
transactionGroups: AuthtypesTransactionGroupsDTO;
}
export interface AuthtypesPostableRotateTokenDTO {
@@ -2258,32 +2242,6 @@ export interface AuthtypesPostableRotateTokenDTO {
refreshToken?: string;
}
export interface AuthtypesPostableUserRoleDTO {
/**
* @type string
*/
id: string;
}
export interface AuthtypesPostableUserDTO {
/**
* @type string
*/
displayName?: string;
/**
* @type string
*/
email: string;
/**
* @type string
*/
frontendBaseUrl?: string;
/**
* @type array
*/
userRoles: AuthtypesPostableUserRoleDTO[];
}
export interface AuthtypesRoleDTO {
/**
* @type string
@@ -2317,40 +2275,6 @@ export interface AuthtypesRoleDTO {
updatedAt?: string;
}
export interface AuthtypesRoleWithTransactionGroupsDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
description: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
transactionGroups: AuthtypesTransactionGroupsDTO;
/**
* @type string
*/
type: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
}
export interface AuthtypesSessionContextDTO {
/**
* @type boolean
@@ -2371,14 +2295,6 @@ export interface AuthtypesUpdatableAuthDomainDTO {
config?: AuthtypesAuthDomainConfigDTO;
}
export interface AuthtypesUpdatableRoleDTO {
/**
* @type string
*/
description: string;
transactionGroups: AuthtypesTransactionGroupsDTO;
}
export interface AuthtypesUserRoleDTO {
/**
* @type string
@@ -3149,6 +3065,14 @@ export interface CommonJSONRefDTO {
$ref?: string;
}
export interface CoretypesObjectGroupDTO {
resource: CoretypesResourceRefDTO;
/**
* @type array
*/
selectors: string[];
}
export interface CoretypesPatchableObjectsDTO {
/**
* @type array,null
@@ -5499,13 +5423,6 @@ export interface InframonitoringtypesClusterRecordDTO {
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
}
export interface InframonitoringtypesRequiredMetricsCheckDTO {
/**
* @type array,null
*/
missingMetrics: string[] | null;
}
export enum InframonitoringtypesResponseTypeDTO {
list = 'list',
grouped_list = 'grouped_list',
@@ -5541,7 +5458,6 @@ export interface InframonitoringtypesClustersDTO {
* @type array
*/
records: InframonitoringtypesClusterRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5619,7 +5535,6 @@ export interface InframonitoringtypesDaemonSetsDTO {
* @type array
*/
records: InframonitoringtypesDaemonSetRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5697,7 +5612,6 @@ export interface InframonitoringtypesDeploymentsDTO {
* @type array
*/
records: InframonitoringtypesDeploymentRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5783,7 +5697,6 @@ export interface InframonitoringtypesHostsDTO {
* @type array
*/
records: InframonitoringtypesHostRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5869,7 +5782,6 @@ export interface InframonitoringtypesJobsDTO {
* @type array
*/
records: InframonitoringtypesJobRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5919,7 +5831,6 @@ export interface InframonitoringtypesNamespacesDTO {
* @type array
*/
records: InframonitoringtypesNamespaceRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5986,7 +5897,6 @@ export interface InframonitoringtypesNodesDTO {
* @type array
*/
records: InframonitoringtypesNodeRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -6070,7 +5980,6 @@ export interface InframonitoringtypesPodsDTO {
* @type array
*/
records: InframonitoringtypesPodRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -6418,7 +6327,6 @@ export interface InframonitoringtypesStatefulSetsDTO {
* @type array
*/
records: InframonitoringtypesStatefulSetRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -6487,7 +6395,6 @@ export interface InframonitoringtypesVolumesDTO {
* @type array
*/
records: InframonitoringtypesVolumeRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -9526,16 +9433,6 @@ export type ListLLMPricingRulesParams = {
* @description undefined
*/
limit?: number;
/**
* @type string
* @description undefined
*/
q?: string;
/**
* @type boolean,null
* @description undefined
*/
isOverride?: boolean | null;
};
export type ListLLMPricingRules200 = {
@@ -9645,7 +9542,7 @@ export type GetRolePathParameters = {
id: string;
};
export type GetRole200 = {
data: AuthtypesRoleWithTransactionGroupsDTO;
data: AuthtypesRoleDTO;
/**
* @type string
*/
@@ -9655,9 +9552,6 @@ export type GetRole200 = {
export type PatchRolePathParameters = {
id: string;
};
export type UpdateRolePathParameters = {
id: string;
};
export type GetObjectsPathParameters = {
id: string;
relation: string;
@@ -10833,14 +10727,6 @@ export type ListUsers200 = {
status: string;
};
export type CreateUser201 = {
data: TypesIdentifiableDTO;
/**
* @type string
*/
status: string;
};
export type GetUserPathParameters = {
id: string;
};

View File

@@ -18,11 +18,9 @@ import type {
} from 'react-query';
import type {
AuthtypesPostableUserDTO,
CreateInvite201,
CreateResetPasswordToken201,
CreateResetPasswordTokenPathParameters,
CreateUser201,
DeleteUserPathParameters,
GetMyUser200,
GetMyUserDeprecated200,
@@ -171,7 +169,6 @@ export const invalidateGetResetPasswordTokenDeprecated = async (
/**
* This endpoint creates an invite for a user
* @deprecated
* @summary Create invite
*/
export const createInvite = (
@@ -233,7 +230,6 @@ export type CreateInviteMutationBody =
export type CreateInviteMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Create invite
*/
export const useCreateInvite = <
@@ -256,7 +252,6 @@ export const useCreateInvite = <
};
/**
* This endpoint creates a bulk invite for a user
* @deprecated
* @summary Create bulk invite
*/
export const createBulkInvite = (
@@ -318,7 +313,6 @@ export type CreateBulkInviteMutationBody =
export type CreateBulkInviteMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Create bulk invite
*/
export const useCreateBulkInvite = <
@@ -424,7 +418,6 @@ export const useResetPassword = <
};
/**
* This endpoint lists all users
* @deprecated
* @summary List users
*/
export const listUsersDeprecated = (signal?: AbortSignal) => {
@@ -470,7 +463,6 @@ export type ListUsersDeprecatedQueryResult = NonNullable<
export type ListUsersDeprecatedQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary List users
*/
@@ -494,7 +486,6 @@ export function useListUsersDeprecated<
}
/**
* @deprecated
* @summary List users
*/
export const invalidateListUsersDeprecated = async (
@@ -590,7 +581,6 @@ export const useDeleteUser = <
};
/**
* This endpoint returns the user by id
* @deprecated
* @summary Get user
*/
export const getUserDeprecated = (
@@ -650,7 +640,6 @@ export type GetUserDeprecatedQueryResult = NonNullable<
export type GetUserDeprecatedQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Get user
*/
@@ -677,7 +666,6 @@ export function useGetUserDeprecated<
}
/**
* @deprecated
* @summary Get user
*/
export const invalidateGetUserDeprecated = async (
@@ -695,7 +683,6 @@ export const invalidateGetUserDeprecated = async (
/**
* This endpoint updates the user by id
* @deprecated
* @summary Update user
*/
export const updateUserDeprecated = (
@@ -768,7 +755,6 @@ export type UpdateUserDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Update user
*/
export const useUpdateUserDeprecated = <
@@ -797,7 +783,6 @@ export const useUpdateUserDeprecated = <
};
/**
* This endpoint returns the user I belong to
* @deprecated
* @summary Get my user
*/
export const getMyUserDeprecated = (signal?: AbortSignal) => {
@@ -843,7 +828,6 @@ export type GetMyUserDeprecatedQueryResult = NonNullable<
export type GetMyUserDeprecatedQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Get my user
*/
@@ -867,7 +851,6 @@ export function useGetMyUserDeprecated<
}
/**
* @deprecated
* @summary Get my user
*/
export const invalidateGetMyUserDeprecated = async (
@@ -1226,89 +1209,6 @@ export const invalidateListUsers = async (
return queryClient;
};
/**
* This endpoint creates a user for the organization
* @summary Create user
*/
export const createUser = (
authtypesPostableUserDTO?: BodyType<AuthtypesPostableUserDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateUser201>({
url: `/api/v2/users`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: authtypesPostableUserDTO,
signal,
});
};
export const getCreateUserMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createUser>>,
TError,
{ data?: BodyType<AuthtypesPostableUserDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createUser>>,
TError,
{ data?: BodyType<AuthtypesPostableUserDTO> },
TContext
> => {
const mutationKey = ['createUser'];
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 createUser>>,
{ data?: BodyType<AuthtypesPostableUserDTO> }
> = (props) => {
const { data } = props ?? {};
return createUser(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateUserMutationResult = NonNullable<
Awaited<ReturnType<typeof createUser>>
>;
export type CreateUserMutationBody =
| BodyType<AuthtypesPostableUserDTO>
| undefined;
export type CreateUserMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create user
*/
export const useCreateUser = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createUser>>,
TError,
{ data?: BodyType<AuthtypesPostableUserDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createUser>>,
TError,
{ data?: BodyType<AuthtypesPostableUserDTO> },
TContext
> => {
return useMutation(getCreateUserMutationOptions(options));
};
/**
* This endpoint returns the user by id
* @summary Get user by user id

View File

@@ -274,110 +274,4 @@ describe('convertV5ResponseToLegacy', () => {
},
});
});
it('clickhouse_sql scalar keeps each value column distinct (regression: all-"A" collapse)', () => {
const scalar: ScalarData = {
columns: [
{
name: 'service.name',
queryName: 'A',
aggregationIndex: 0,
columnType: 'group',
} as unknown as ScalarData['columns'][number],
{
name: 'current_availability',
queryName: 'A',
aggregationIndex: 0,
columnType: 'aggregation',
} as unknown as ScalarData['columns'][number],
{
name: 'error_budget_remaining',
queryName: 'A',
aggregationIndex: 1,
columnType: 'aggregation',
} as unknown as ScalarData['columns'][number],
{
name: 'budget_status',
queryName: 'A',
aggregationIndex: 2,
columnType: 'group',
} as unknown as ScalarData['columns'][number],
{
name: 'total_requests',
queryName: 'A',
aggregationIndex: 4,
columnType: 'aggregation',
} as unknown as ScalarData['columns'][number],
],
data: [['kuja-api_gateway-service', 99.985, 0.985, 'Healthy ✅', 2181216]],
};
const v5Data: QueryRangeResponseV5 = {
type: 'scalar',
data: { results: [scalar] },
meta: { rowsScanned: 0, bytesScanned: 0, durationMs: 0, stepIntervals: {} },
};
// A clickhouse_sql envelope contributes no aggregation metadata.
const params = makeBaseParams('scalar', [
{
type: 'clickhouse_sql',
spec: {
name: 'A',
query: 'SELECT ...',
disabled: false,
},
} as unknown as QueryRangeRequestV5['compositeQuery']['queries'][number],
]);
const input: SuccessResponse<MetricRangePayloadV5, QueryRangeRequestV5> =
makeBaseSuccess({ data: v5Data }, params);
// formatForWeb=true is the table-panel path.
const result = convertV5ResponseToLegacy(input, { A: '' }, true);
const [tableEntry] = result.payload.data.result;
// Headers keep their real names instead of collapsing to "A".
expect(tableEntry.table?.columns).toStrictEqual([
{
name: 'service.name',
queryName: 'A',
isValueColumn: false,
id: 'service.name',
},
{
name: 'current_availability',
queryName: 'A',
isValueColumn: true,
id: 'current_availability',
},
{
name: 'error_budget_remaining',
queryName: 'A',
isValueColumn: true,
id: 'error_budget_remaining',
},
{
name: 'budget_status',
queryName: 'A',
isValueColumn: false,
id: 'budget_status',
},
{
name: 'total_requests',
queryName: 'A',
isValueColumn: true,
id: 'total_requests',
},
]);
// Ids are unique, so value columns don't overwrite each other in the row.
expect(tableEntry.table?.rows?.[0]).toStrictEqual({
data: {
'service.name': 'kuja-api_gateway-service',
current_availability: 99.985,
error_budget_remaining: 0.985,
budget_status: 'Healthy ✅',
total_requests: 2181216,
},
});
});
});

View File

@@ -15,7 +15,6 @@ function getColName(
col: ScalarData['columns'][number],
legendMap: Record<string, string>,
aggregationPerQuery: Record<string, any>,
clickhouseQueryNames: Set<string>,
): string {
if (col.columnType === 'group') {
return col.name;
@@ -40,32 +39,16 @@ function getColName(
return alias || expression || col.queryName;
}
// clickhouse_sql value columns carry their real SQL alias in col.name — use
// it so each value column keeps its own header instead of collapsing onto
// the query name. Formulas/promql use placeholder names, so they fall back
// to legend || queryName.
if (clickhouseQueryNames.has(col.queryName)) {
return col.name;
}
return legend || col.queryName;
}
function getColId(
col: ScalarData['columns'][number],
aggregationPerQuery: Record<string, any>,
clickhouseQueryNames: Set<string>,
): string {
if (col.columnType === 'group') {
return col.name;
}
// clickhouse_sql value columns are keyed by their real SQL alias so multiple
// value columns stay unique instead of all collapsing onto the query name
// (which would overwrite every cell in the row with the last column's value).
if (clickhouseQueryNames.has(col.queryName)) {
return col.name;
}
const aggregation =
aggregationPerQuery?.[col.queryName]?.[col.aggregationIndex];
const expression = aggregation?.expression || '';
@@ -158,7 +141,6 @@ function convertScalarDataArrayToTable(
scalarDataArray: ScalarData[],
legendMap: Record<string, string>,
aggregationPerQuery: Record<string, any>,
clickhouseQueryNames: Set<string>,
): QueryDataV3[] {
// If no scalar data, return empty structure
@@ -184,10 +166,10 @@ function convertScalarDataArrayToTable(
// Collect columns for this specific query
const columns = scalarData?.columns?.map((col) => ({
name: getColName(col, legendMap, aggregationPerQuery, clickhouseQueryNames),
name: getColName(col, legendMap, aggregationPerQuery),
queryName: col.queryName,
isValueColumn: col.columnType === 'aggregation',
id: getColId(col, aggregationPerQuery, clickhouseQueryNames),
id: getColId(col, aggregationPerQuery),
}));
// Process rows for this specific query
@@ -195,13 +177,8 @@ function convertScalarDataArrayToTable(
const rowData: Record<string, any> = {};
scalarData?.columns?.forEach((col, colIndex) => {
const columnName = getColName(
col,
legendMap,
aggregationPerQuery,
clickhouseQueryNames,
);
const columnId = getColId(col, aggregationPerQuery, clickhouseQueryNames);
const columnName = getColName(col, legendMap, aggregationPerQuery);
const columnId = getColId(col, aggregationPerQuery);
rowData[columnId || columnName] = dataRow[colIndex];
});
@@ -225,7 +202,6 @@ function convertScalarWithFormatForWeb(
scalarDataArray: ScalarData[],
legendMap: Record<string, string>,
aggregationPerQuery: Record<string, any>,
clickhouseQueryNames: Set<string>,
): QueryDataV3[] {
if (!scalarDataArray || scalarDataArray.length === 0) {
return [];
@@ -234,18 +210,13 @@ function convertScalarWithFormatForWeb(
return scalarDataArray.map((scalarData) => {
const columns =
scalarData.columns?.map((col) => {
const colName = getColName(
col,
legendMap,
aggregationPerQuery,
clickhouseQueryNames,
);
const colName = getColName(col, legendMap, aggregationPerQuery);
return {
name: colName,
queryName: col.queryName,
isValueColumn: col.columnType === 'aggregation',
id: getColId(col, aggregationPerQuery, clickhouseQueryNames),
id: getColId(col, aggregationPerQuery),
};
}) || [];
@@ -318,7 +289,6 @@ function convertV5DataByType(
v5Data: any,
legendMap: Record<string, string>,
aggregationPerQuery: Record<string, any>,
clickhouseQueryNames: Set<string>,
): MetricRangePayloadV3['data'] {
switch (v5Data?.type) {
case 'time_series': {
@@ -337,7 +307,6 @@ function convertV5DataByType(
scalarData,
legendMap,
aggregationPerQuery,
clickhouseQueryNames,
);
return {
resultType: 'scalar',
@@ -404,15 +373,6 @@ export function convertV5ResponseToLegacy(
{} as Record<string, any>,
) || {};
// clickhouse_sql queries have no aggregation metadata; their value columns
// are named/keyed by the real SQL alias the response carries (see getColId).
const clickhouseQueryNames = new Set<string>(
(params?.compositeQuery?.queries ?? [])
.filter((query) => query.type === 'clickhouse_sql')
.map((query) => (query.spec as { name?: string })?.name)
.filter((name): name is string => !!name),
);
// If formatForWeb is true, return as-is (like existing logic)
if (formatForWeb && v5Data?.type === 'scalar') {
const scalarData = v5Data.data.results as ScalarData[];
@@ -420,7 +380,6 @@ export function convertV5ResponseToLegacy(
scalarData,
legendMap,
aggregationPerQuery,
clickhouseQueryNames,
);
return {
@@ -443,7 +402,6 @@ export function convertV5ResponseToLegacy(
v5Data,
legendMap,
aggregationPerQuery,
clickhouseQueryNames,
);
// Create legacy-compatible response structure

View File

@@ -12,5 +12,4 @@ export enum FeatureKeys {
USE_JSON_BODY = 'use_json_body',
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
USE_DASHBOARD_V2 = 'use_dashboard_v2',
EMABLE_AI_OBSERVABILITY = 'enable_ai_observability',
}

View File

@@ -43,5 +43,4 @@ export enum LOCALSTORAGE {
DASHBOARD_PREFERENCES = 'DASHBOARD_PREFERENCES',
ACTIVE_SIGNOZ_INSTANCE_URL = 'ACTIVE_SIGNOZ_INSTANCE_URL',
DASHBOARDS_LIST_VISIBLE_COLUMNS = 'DASHBOARDS_LIST_VISIBLE_COLUMNS',
DASHBOARDS_LIST_VIEWS = 'DASHBOARDS_LIST_VIEWS',
}

View File

@@ -116,8 +116,7 @@ function CreateRoleModal({
} else {
const data: AuthtypesPostableRoleDTO = {
name: values.name,
description: values.description || '',
transactionGroups: [],
...(values.description ? { description: values.description } : {}),
};
createRole({ data });
}

View File

@@ -20,7 +20,7 @@ import APIError from 'types/api/error';
import DashboardActions from './DashboardActions/DashboardActions';
import DashboardInfo from './DashboardInfo/DashboardInfo';
import { useEditableTitle } from './DashboardInfo/useEditableTitle';
import VariablesBar from '../VariablesBar/VariablesBar';
import { usePublicDashboardMeta } from '../DashboardSettings/PublicDashboard/usePublicDashboardMeta';
import styles from './DashboardPageToolbar.module.scss';
@@ -53,6 +53,10 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
(s) => s.setIsPanelTypeSelectionModalOpen,
);
// Single global fetch of the public-sharing meta (the drawer reuses this cache);
// drives the public-access badge.
const { isPublic: isPublicDashboard } = usePublicDashboardMeta(id);
const isAuthor =
!!user?.email && !!dashboard.createdBy && dashboard.createdBy === user.email;
@@ -118,7 +122,7 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
image={image}
tags={tags}
description={description}
isPublicDashboard={false}
isPublicDashboard={isPublicDashboard}
isDashboardLocked={isDashboardLocked}
isEditing={isEditing}
draft={draft}
@@ -138,8 +142,6 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
onOpenRename={startEdit}
/>
</div>
<VariablesBar dashboard={dashboard} />
</section>
);
}

View File

@@ -1,4 +1,3 @@
import { sortBy } from 'lodash-es';
import type { VariableDefaultValueDTO } from 'api/generated/services/sigNoz.schemas';
/**
@@ -77,26 +76,3 @@ export function emptyVariableFormModel(): VariableFormModel {
dynamicSignal: 'traces',
};
}
/** Maps the dynamic-variable signal to the field-values API signal. */
export function signalForApi(
signal: TelemetrySignal,
): TelemetrySignal | undefined {
return signal;
}
type SortableValues = (string | number | boolean)[];
/** Sorts option/preview values by the variable's chosen order (no-op when disabled). */
export function sortValuesByOrder(
values: SortableValues,
sort: VariableSort,
): SortableValues {
if (sort === 'ASC') {
return sortBy(values);
}
if (sort === 'DESC') {
return sortBy(values).reverse();
}
return values;
}

View File

@@ -1,114 +0,0 @@
import { useMemo } from 'react';
import { SolidInfoCircle } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
// eslint-disable-next-line signoz/no-antd-components -- lightweight description tooltip, matches V1
import { Tooltip } from 'antd';
import { commaValuesParser } from 'lib/dashboardVariables/customCommaValuesParser';
import { sortValuesByOrder } from '../DashboardSettings/Variables/variableModel';
import type { VariableFormModel } from '../DashboardSettings/Variables/variableModel';
import type { VariableSelection, VariableSelectionMap } from './selectionTypes';
import DynamicSelector from './selectors/DynamicSelector';
import QuerySelector from './selectors/QuerySelector';
import TextSelector from './selectors/TextSelector';
import ValueSelector from './selectors/ValueSelector';
import styles from './VariablesBar.module.scss';
interface VariableSelectorProps {
variable: VariableFormModel;
/** All variables (Dynamic uses them to scope options by sibling selections). */
variables: VariableFormModel[];
/** Names this variable depends on (for Query gating). */
parents: string[];
/** All current selections (Query passes them as the request payload). */
selections: VariableSelectionMap;
selection: VariableSelection;
onChange: (selection: VariableSelection) => void;
}
/** One labelled variable control; dispatches on the variable type. */
function VariableSelector({
variable,
variables,
parents,
selections,
selection,
onChange,
}: VariableSelectorProps): JSX.Element {
const customOptions = useMemo(
() =>
variable.type === 'CUSTOM'
? sortValuesByOrder(
commaValuesParser(variable.customValue),
variable.sort,
).map(String)
: [],
[variable],
);
const renderControl = (): JSX.Element => {
switch (variable.type) {
case 'TEXT':
return (
<TextSelector
selection={selection}
defaultValue={variable.textValue}
onChange={onChange}
testId={`variable-input-${variable.name}`}
/>
);
case 'QUERY':
return (
<QuerySelector
variable={variable}
parents={parents}
selections={selections}
selection={selection}
onChange={onChange}
/>
);
case 'DYNAMIC':
return (
<DynamicSelector
variable={variable}
variables={variables}
selections={selections}
selection={selection}
onChange={onChange}
/>
);
case 'CUSTOM':
default:
return (
<ValueSelector
options={customOptions}
multiSelect={variable.multiSelect}
showAllOption={variable.showAllOption}
selection={selection}
onChange={onChange}
testId={`variable-select-${variable.name}`}
/>
);
}
};
return (
<div
className={styles.variableItem}
data-testid={`variable-${variable.name}`}
>
<Typography.Text className={styles.variableName}>
${variable.name}
{variable.description ? (
<Tooltip title={variable.description}>
<SolidInfoCircle className={styles.infoIcon} size="md" />
</Tooltip>
) : null}
</Typography.Text>
<div className={styles.variableValue}>{renderControl()}</div>
</div>
);
}
export default VariableSelector;

View File

@@ -1,71 +0,0 @@
/* Mirrors the V1 dashboard variable bar: each variable is a connected pill —
a robin `$name` segment joined to a value segment. */
/* Sits inside the already-padded sticky toolbar section, so it only needs a top
gap from the tags — horizontal/bottom padding comes from the toolbar. */
.bar {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding-top: 12px;
}
.variableItem {
display: flex;
align-items: center;
}
.variableName {
display: flex;
min-width: 56px;
height: 32px;
align-items: center;
gap: 4px;
padding: 6px 6px 6px 8px;
border: 1px solid var(--l1-border);
border-radius: 2px 0 0 2px;
background: var(--l3-background);
color: var(--bg-robin-300);
font-family: Inter;
font-size: 12px;
font-weight: 400;
line-height: 16px;
white-space: nowrap;
}
.infoIcon {
margin-left: 4px;
color: var(--l2-foreground);
}
.variableValue {
display: flex;
min-width: 120px;
height: 32px;
align-items: center;
border: 1px solid var(--l1-border);
border-left: none;
border-radius: 0 2px 2px 0;
background: var(--l2-background);
color: var(--l2-foreground);
font-size: 12px;
&:hover,
&:focus-within {
outline: 1px solid var(--bg-robin-400);
}
}
/* Inner control fills the value segment; the segment provides the frame, so the
control itself is borderless/transparent. */
.control {
width: 100%;
min-width: 120px;
:global(.ant-select-selector),
:global(.ant-input),
&:global(.ant-input) {
border: none !important;
background: transparent !important;
box-shadow: none !important;
}
}

View File

@@ -1,45 +0,0 @@
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import { useVariableSelection } from './useVariableSelection';
import VariableSelector from './VariableSelector';
import styles from './VariablesBar.module.scss';
interface VariablesBarProps {
dashboard: DashboardtypesGettableDashboardV2DTO;
}
/**
* Runtime variable selector bar shown above the panels. Renders one control per
* dashboard variable; selections live in the store + URL (never the spec).
*/
function VariablesBar({ dashboard }: VariablesBarProps): JSX.Element | null {
const { variables, dependencyData, selection, setSelection } =
useVariableSelection(dashboard);
if (variables.length === 0) {
return null;
}
return (
<div className={styles.bar} data-testid="dashboard-variables-bar">
{variables.map((variable) => (
<VariableSelector
key={variable.name}
variable={variable}
variables={variables}
parents={dependencyData.parentGraph[variable.name] ?? []}
selections={selection}
selection={
selection[variable.name] ?? {
value: variable.multiSelect ? [] : '',
allSelected: false,
}
}
onChange={(next): void => setSelection(variable.name, next)}
/>
))}
</div>
);
}
export default VariablesBar;

View File

@@ -1,56 +0,0 @@
import type { VariableFormModel } from '../DashboardSettings/Variables/variableModel';
import type { VariableSelectionMap } from './selectionTypes';
function formatQueryValue(val: string): string {
const num = Number(val);
if (!Number.isNaN(num) && Number.isFinite(num)) {
return val;
}
return `'${val.replace(/'/g, "\\'")}'`;
}
function buildQueryPart(attribute: string, values: string[]): string {
const formatted = values.map(formatQueryValue);
if (formatted.length === 1) {
return `${attribute} = ${formatted[0]}`;
}
return `${attribute} IN [${formatted.join(', ')}]`;
}
/**
* Builds a filter expression from the OTHER dynamic variables' current
* selections (e.g. `k8s.namespace.name IN ['prod'] AND service = 'api'`), so a
* dynamic variable's option list is scoped by its sibling selections. Variables
* in the ALL state, with no selection, or non-dynamic are skipped. Ported from
* the V1 dynamic-variable runtime.
*/
export function buildExistingDynamicVariableQuery(
variables: VariableFormModel[],
selections: VariableSelectionMap,
currentName: string,
): string {
const parts: string[] = [];
variables.forEach((variable) => {
if (
variable.name === currentName ||
variable.type !== 'DYNAMIC' ||
!variable.dynamicAttribute
) {
return;
}
const selection = selections[variable.name];
if (!selection || selection.allSelected) {
return;
}
const raw = Array.isArray(selection.value)
? selection.value
: [selection.value];
const valid = raw
.filter((v) => v !== null && v !== undefined && v !== '')
.map((v) => String(v));
if (valid.length > 0) {
parts.push(buildQueryPart(variable.dynamicAttribute, valid));
}
});
return parts.join(' AND ');
}

View File

@@ -1,16 +0,0 @@
/** A user-selected variable value at runtime (not persisted to the spec). */
export type SelectedVariableValue =
| string
| number
| boolean
| (string | number | boolean)[]
| null;
export interface VariableSelection {
value: SelectedVariableValue;
/** True when every option is selected ("ALL"); for dynamic vars value may be null. */
allSelected: boolean;
}
/** Selected values for a dashboard's variables, keyed by variable name. */
export type VariableSelectionMap = Record<string, VariableSelection>;

View File

@@ -1,31 +0,0 @@
import type {
SelectedVariableValue,
VariableSelection,
VariableSelectionMap,
} from './selectionTypes';
/** A selection counts as resolved (usable as a parent value) when it's non-empty. */
export function isResolved(selection?: VariableSelection): boolean {
if (!selection) {
return false;
}
if (selection.allSelected) {
return true;
}
const { value } = selection;
if (Array.isArray(value)) {
return value.length > 0;
}
return value !== '' && value !== null && value !== undefined;
}
/** Flatten the selection map into the `{ name: value }` payload a query expects. */
export function selectionToPayload(
selection: VariableSelectionMap,
): Record<string, SelectedVariableValue> {
const payload: Record<string, SelectedVariableValue> = {};
Object.entries(selection).forEach(([name, sel]) => {
payload[name] = sel.value;
});
return payload;
}

View File

@@ -1,82 +0,0 @@
import { useMemo } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useGetFieldValues } from 'hooks/dynamicVariables/useGetFieldValues';
import type { AppState } from 'store/reducers';
import type { GlobalReducer } from 'types/reducer/globalTime';
import {
signalForApi,
sortValuesByOrder,
} from '../../DashboardSettings/Variables/variableModel';
import type { VariableFormModel } from '../../DashboardSettings/Variables/variableModel';
import { buildExistingDynamicVariableQuery } from '../dynamicFilter';
import type {
VariableSelection,
VariableSelectionMap,
} from '../selectionTypes';
import { useAutoSelect } from '../useAutoSelect';
import ValueSelector from './ValueSelector';
interface DynamicSelectorProps {
variable: VariableFormModel;
/** All variables + current selections, to scope options by sibling dynamics. */
variables: VariableFormModel[];
selections: VariableSelectionMap;
selection: VariableSelection;
onChange: (selection: VariableSelection) => void;
}
/**
* Dynamic-variable options sourced from live telemetry field values for the
* chosen signal + attribute, scoped by the other dynamic variables' selections
* (so e.g. `pod` narrows to the chosen `namespace`).
*/
function DynamicSelector({
variable,
variables,
selections,
selection,
onChange,
}: DynamicSelectorProps): JSX.Element {
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const existingQuery = useMemo(
() => buildExistingDynamicVariableQuery(variables, selections, variable.name),
[variables, selections, variable.name],
);
const { data, isFetching } = useGetFieldValues({
signal: signalForApi(variable.dynamicSignal),
name: variable.dynamicAttribute,
startUnixMilli: minTime,
endUnixMilli: maxTime,
existingQuery: existingQuery || undefined,
enabled: !!variable.dynamicAttribute,
});
const options = useMemo(() => {
const payload = data?.data;
const values =
payload?.normalizedValues ?? payload?.values?.StringValues ?? [];
return sortValuesByOrder(values, variable.sort).map(String);
}, [data, variable.sort]);
useAutoSelect(variable, options, selection, onChange);
return (
<ValueSelector
options={options}
multiSelect={variable.multiSelect}
showAllOption={variable.showAllOption}
loading={isFetching}
selection={selection}
onChange={onChange}
testId={`variable-select-${variable.name}`}
/>
);
}
export default DynamicSelector;

View File

@@ -1,90 +0,0 @@
import { useMemo } from 'react';
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import type { AppState } from 'store/reducers';
import type { GlobalReducer } from 'types/reducer/globalTime';
import { sortValuesByOrder } from '../../DashboardSettings/Variables/variableModel';
import type { VariableFormModel } from '../../DashboardSettings/Variables/variableModel';
import type {
VariableSelection,
VariableSelectionMap,
} from '../selectionTypes';
import { isResolved, selectionToPayload } from '../selectionUtils';
import { useAutoSelect } from '../useAutoSelect';
import ValueSelector from './ValueSelector';
interface QuerySelectorProps {
variable: VariableFormModel;
/** Names this variable's query references; it waits until they're resolved. */
parents: string[];
/** All current selections, fed to the query as `{ name: value }`. */
selections: VariableSelectionMap;
selection: VariableSelection;
onChange: (selection: VariableSelection) => void;
}
/**
* Query-driven options. Dependency orchestration is declarative: the query is
* `enabled` only once every parent is resolved, and the parent values are in the
* query key — so it refetches automatically when a parent changes (and a cyclic
* dependency is simply never enabled).
*/
function QuerySelector({
variable,
parents,
selections,
selection,
onChange,
}: QuerySelectorProps): JSX.Element {
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const payload = useMemo(() => selectionToPayload(selections), [selections]);
const enabled = parents.every((parent) => isResolved(selections[parent]));
const { data, isFetching } = useQuery(
[
'dashboard-variable',
variable.name,
variable.queryValue,
payload,
minTime,
maxTime,
],
() =>
dashboardVariablesQuery({
query: variable.queryValue,
variables: payload,
}),
{ enabled, refetchOnWindowFocus: false },
);
const options = useMemo(() => {
if (!data || data.statusCode !== 200 || !data.payload) {
return [] as string[];
}
return sortValuesByOrder(
data.payload.variableValues ?? [],
variable.sort,
).map(String);
}, [data, variable.sort]);
useAutoSelect(variable, options, selection, onChange);
return (
<ValueSelector
options={options}
multiSelect={variable.multiSelect}
showAllOption={variable.showAllOption}
loading={isFetching}
selection={selection}
onChange={onChange}
testId={`variable-select-${variable.name}`}
/>
);
}
export default QuerySelector;

View File

@@ -1,70 +0,0 @@
import { useCallback, useRef, useState } from 'react';
import type { InputRef } from 'antd';
// eslint-disable-next-line signoz/no-antd-components -- match V1 textbox behaviour (commit on blur/Enter, borderless)
import { Input } from 'antd';
import type { VariableSelection } from '../selectionTypes';
import styles from '../VariablesBar.module.scss';
interface TextSelectorProps {
selection: VariableSelection;
/** Configured default; an emptied input falls back to it (V1 behaviour). */
defaultValue?: string;
onChange: (selection: VariableSelection) => void;
testId?: string;
}
/**
* Free-text variable input. Mirrors V1: edits are local and only committed on
* blur / Enter (not per keystroke), and clearing the field restores the default.
*/
function TextSelector({
selection,
defaultValue,
onChange,
testId,
}: TextSelectorProps): JSX.Element {
const inputRef = useRef<InputRef>(null);
const [value, setValue] = useState<string>(
typeof selection.value === 'string' ? selection.value : (defaultValue ?? ''),
);
const commit = useCallback(
(next: string): void => onChange({ value: next, allSelected: false }),
[onChange],
);
const handleBlur = useCallback(
(event: React.FocusEvent<HTMLInputElement>): void => {
const trimmed = event.target.value.trim();
if (!trimmed && defaultValue) {
setValue(defaultValue);
commit(defaultValue);
} else {
commit(trimmed);
}
},
[commit, defaultValue],
);
return (
<Input
ref={inputRef}
className={styles.control}
bordered={false}
placeholder="Enter value"
value={value}
title={value}
onChange={(e): void => setValue(e.target.value)}
onBlur={handleBlur}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
inputRef.current?.blur();
}
}}
data-testid={testId}
/>
);
}
export default TextSelector;

View File

@@ -1,94 +0,0 @@
import { useMemo } from 'react';
import { CustomMultiSelect, CustomSelect } from 'components/NewSelect';
import type { OptionData } from 'components/NewSelect/types';
import { ALL_SELECT_VALUE } from 'container/DashboardContainer/utils';
import type { VariableSelection } from '../selectionTypes';
import styles from '../VariablesBar.module.scss';
interface ValueSelectorProps {
options: string[];
multiSelect: boolean;
showAllOption: boolean;
loading?: boolean;
selection: VariableSelection;
onChange: (selection: VariableSelection) => void;
testId?: string;
}
/**
* Single/multi value picker for Custom/Query/Dynamic variables. Reuses the
* shared NewSelect components, which provide search, the "ALL" option and
* apply-on-close batching (so multi-select edits don't cascade per toggle).
*/
function ValueSelector({
options,
multiSelect,
showAllOption,
loading,
selection,
onChange,
testId,
}: ValueSelectorProps): JSX.Element {
const optionData = useMemo<OptionData[]>(
() => options.map((option) => ({ label: option, value: option })),
[options],
);
if (multiSelect) {
const value = selection.allSelected
? ALL_SELECT_VALUE
: (Array.isArray(selection.value) ? selection.value : []).map(String);
return (
<CustomMultiSelect
className={styles.control}
data-testid={testId}
options={optionData}
value={value}
loading={loading}
showSearch
placeholder="Select value"
enableAllSelection={showAllOption}
onChange={(next): void => {
const values = Array.isArray(next)
? next.map(String)
: next
? [String(next)]
: [];
if (values.length === 0) {
onChange({ value: [], allSelected: false });
return;
}
// CustomMultiSelect emits the full value set when ALL is picked.
const isAll =
showAllOption &&
options.length > 0 &&
options.every((option) => values.includes(option));
onChange({ value: values, allSelected: isAll });
}}
onClear={(): void => onChange({ value: [], allSelected: false })}
/>
);
}
return (
<CustomSelect
className={styles.select}
data-testid={testId}
options={optionData}
value={
selection.value == null || Array.isArray(selection.value)
? undefined
: String(selection.value)
}
loading={loading}
showSearch
placeholder="Select value"
onChange={(next): void =>
onChange({ value: next == null ? '' : String(next), allSelected: false })
}
/>
);
}
export default ValueSelector;

View File

@@ -1,41 +0,0 @@
import { useEffect } from 'react';
import type { VariableFormModel } from '../DashboardSettings/Variables/variableModel';
import type { VariableSelection } from './selectionTypes';
/**
* When fetched options arrive and the current selection isn't one of them,
* auto-pick the variable's default (if present in the options) or the first
* option — so dependent children always have a usable parent value.
*/
export function useAutoSelect(
variable: VariableFormModel,
options: string[],
selection: VariableSelection,
onChange: (selection: VariableSelection) => void,
): void {
useEffect(() => {
if (options.length === 0 || selection.allSelected) {
return;
}
const current = selection.value;
const isValid = Array.isArray(current)
? current.length > 0 && current.every((c) => options.includes(String(c)))
: current !== '' &&
current !== null &&
current !== undefined &&
options.includes(String(current));
if (isValid) {
return;
}
const fallback = (variable.defaultValue as { value?: string } | undefined)
?.value;
const initial =
fallback && options.includes(fallback) ? fallback : options[0];
onChange({
value: variable.multiSelect ? [initial] : initial,
allSelected: false,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [options]);
}

View File

@@ -1,116 +0,0 @@
import { useCallback, useEffect, useMemo } from 'react';
import { parseAsJson, useQueryState } from 'nuqs';
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import { dtoToFormModel } from '../DashboardSettings/Variables/variableAdapters';
import type { VariableFormModel } from '../DashboardSettings/Variables/variableModel';
import { selectVariableValues } from '../store/slices/variableSelectionSlice';
import { useDashboardStore } from '../store/useDashboardStore';
import type {
SelectedVariableValue,
VariableSelection,
VariableSelectionMap,
} from './selectionTypes';
import {
computeVariableDependencies,
type VariableDependencyData,
} from './variableDependencies';
/** URL sentinel for an "ALL values selected" state (matches V1). */
export const ALL_SELECTED = '__ALL__';
/** `?variables=` holds `{ [name]: value }` (ALL encoded as the sentinel). */
const variablesUrlParser = parseAsJson<Record<string, SelectedVariableValue>>(
(v) =>
typeof v === 'object' && v !== null
? (v as Record<string, SelectedVariableValue>)
: null,
);
function defaultSelection(model: VariableFormModel): VariableSelection {
const def = (
model.defaultValue as { value?: SelectedVariableValue } | undefined
)?.value;
if (def !== undefined && def !== null && def !== '') {
return { value: def, allSelected: false };
}
return { value: model.multiSelect ? [] : '', allSelected: false };
}
function fromUrlValue(raw: SelectedVariableValue): VariableSelection {
return raw === ALL_SELECTED
? { value: null, allSelected: true }
: { value: raw, allSelected: false };
}
interface UseVariableSelection {
variables: VariableFormModel[];
dependencyData: VariableDependencyData;
selection: VariableSelectionMap;
setSelection: (name: string, selection: VariableSelection) => void;
}
/**
* Runtime variable selection: derives the variable list from the spec, seeds
* each value from URL → localStorage(store) → default, and persists changes to
* both the store and the URL. Never writes to the dashboard spec.
*/
export function useVariableSelection(
dashboard: DashboardtypesGettableDashboardV2DTO,
): UseVariableSelection {
const dashboardId = dashboard.id ?? '';
const variables = useMemo(
() => (dashboard.spec?.variables ?? []).map(dtoToFormModel),
[dashboard.spec?.variables],
);
const dependencyData = useMemo(
() => computeVariableDependencies(variables),
[variables],
);
const selection = useDashboardStore(selectVariableValues(dashboardId));
const setVariableValue = useDashboardStore((s) => s.setVariableValue);
const setVariableValues = useDashboardStore((s) => s.setVariableValues);
const [urlValues, setUrlValues] = useQueryState(
'variables',
variablesUrlParser.withOptions({ history: 'replace' }),
);
// Seed selections for this dashboard: URL wins, then persisted store, then default.
useEffect(() => {
if (!dashboardId || variables.length === 0) {
return;
}
// `selection` here is the persisted (localStorage) map on mount — the
// effect deliberately doesn't depend on it, so seeding runs once per set.
const stored = selection;
const seeded: VariableSelectionMap = {};
variables.forEach((variable) => {
const urlValue = urlValues?.[variable.name];
if (urlValue !== undefined) {
seeded[variable.name] = fromUrlValue(urlValue);
} else if (stored[variable.name]) {
seeded[variable.name] = stored[variable.name];
} else {
seeded[variable.name] = defaultSelection(variable);
}
});
setVariableValues(dashboardId, seeded);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dashboardId, variables]);
const setSelection = useCallback(
(name: string, next: VariableSelection): void => {
setVariableValue(dashboardId, name, next);
void setUrlValues((prev) => ({
...(prev ?? {}),
[name]: next.allSelected ? ALL_SELECTED : next.value,
}));
},
[dashboardId, setVariableValue, setUrlValues],
);
return { variables, dependencyData, selection, setSelection };
}

View File

@@ -1,199 +0,0 @@
import { textContainsVariableReference } from 'lib/dashboardVariables/variableReference';
import type { VariableFormModel } from '../DashboardSettings/Variables/variableModel';
/**
* Inter-variable dependency graph for runtime selection. A QUERY variable
* "depends on" another variable when its query text references that variable
* (`{{.name}}`, `{{name}}`, `$name`, `[[name]]`). When a variable's value
* changes, its dependent QUERY variables must refetch. Ported from the V1
* dashboard-variables runtime; operates on the V2 flat variable model.
*/
export type VariableGraph = Record<string, string[]>;
export interface VariableDependencyData {
/** Topological order of variables (parents before children). */
order: string[];
/** Direct children (dependents) of each variable. */
graph: VariableGraph;
/** Direct parents of each variable. */
parentGraph: VariableGraph;
/** All transitive descendants of each variable (precomputed). */
transitiveDescendants: VariableGraph;
hasCycle: boolean;
cycleNodes?: string[];
}
/** Names of QUERY variables whose query references `variableName`. */
function getDependents(
variableName: string,
variables: VariableFormModel[],
): string[] {
return variables
.filter(
(v) =>
v.type === 'QUERY' &&
!!v.name &&
textContainsVariableReference(v.queryValue || '', variableName),
)
.map((v) => v.name);
}
/** variable name → its direct dependents (children). */
export function buildDependencies(
variables: VariableFormModel[],
): VariableGraph {
const graph: VariableGraph = {};
variables.forEach((v) => {
if (v.name) {
graph[v.name] = getDependents(v.name, variables);
}
});
return graph;
}
/** Invert a child graph into a parent graph. */
export function buildParentGraph(graph: VariableGraph): VariableGraph {
const parents: VariableGraph = {};
Object.keys(graph).forEach((node) => {
parents[node] = parents[node] ?? [];
});
Object.entries(graph).forEach(([node, children]) => {
children.forEach((child) => {
parents[child] = parents[child] ?? [];
parents[child].push(node);
});
});
return parents;
}
function collectCyclePath(
graph: VariableGraph,
start: string,
end: string,
): string[] {
const path: string[] = [];
let current = start;
const findParent = (node: string): string | undefined =>
Object.keys(graph).find((key) => graph[key]?.includes(node));
while (current !== end) {
const parent = findParent(current);
if (!parent) {
break;
}
path.push(parent);
current = parent;
}
return [start, ...path];
}
function detectCycle(
graph: VariableGraph,
node: string,
visited: Set<string>,
recStack: Set<string>,
): string[] | null {
if (!visited.has(node)) {
visited.add(node);
recStack.add(node);
let cycleNodes: string[] | null = null;
(graph[node] || []).some((neighbor) => {
if (!visited.has(neighbor)) {
const found = detectCycle(graph, neighbor, visited, recStack);
if (found) {
cycleNodes = found;
return true;
}
} else if (recStack.has(neighbor)) {
cycleNodes = collectCyclePath(graph, node, neighbor);
return true;
}
return false;
});
if (cycleNodes) {
return cycleNodes;
}
}
recStack.delete(node);
return null;
}
/** Build the full dependency data (topo order, parents, transitive descendants, cycle info). */
export function buildDependencyData(
dependencies: VariableGraph,
): VariableDependencyData {
const inDegree: Record<string, number> = {};
const adjList: VariableGraph = {};
Object.keys(dependencies).forEach((node) => {
inDegree[node] = inDegree[node] ?? 0;
adjList[node] = adjList[node] ?? [];
(dependencies[node] || []).forEach((child) => {
inDegree[child] = inDegree[child] ?? 0;
inDegree[child] += 1;
adjList[node].push(child);
});
});
const visited = new Set<string>();
const recStack = new Set<string>();
let cycleNodes: string[] | undefined;
Object.keys(dependencies).some((node) => {
if (!visited.has(node)) {
const found = detectCycle(dependencies, node, visited, recStack);
if (found) {
cycleNodes = found;
return true;
}
}
return false;
});
// Topological sort (Kahn's algorithm).
const queue = Object.keys(inDegree).filter((n) => inDegree[n] === 0);
const order: string[] = [];
while (queue.length > 0) {
const current = queue.shift();
if (current === undefined) {
break;
}
order.push(current);
(adjList[current] || []).forEach((neighbor) => {
inDegree[neighbor] -= 1;
if (inDegree[neighbor] === 0) {
queue.push(neighbor);
}
});
}
const hasCycle = order.length !== Object.keys(dependencies).length;
// Transitive descendants: walk topo order in reverse.
const transitiveDescendants: VariableGraph = {};
for (let i = order.length - 1; i >= 0; i--) {
const node = order[i];
const desc = new Set<string>();
(adjList[node] || []).forEach((child) => {
desc.add(child);
(transitiveDescendants[child] || []).forEach((d) => desc.add(d));
});
transitiveDescendants[node] = Array.from(desc);
}
return {
order,
graph: adjList,
parentGraph: buildParentGraph(adjList),
transitiveDescendants,
hasCycle,
cycleNodes,
};
}
/** Compute the full dependency data straight from the variable list. */
export function computeVariableDependencies(
variables: VariableFormModel[],
): VariableDependencyData {
return buildDependencyData(buildDependencies(variables));
}

View File

@@ -5,7 +5,6 @@ import type {
import {
extractAggregationsPerQuery,
extractClickhouseQueryNames,
prepareScalarTables,
} from '../prepareScalarTables';
@@ -57,24 +56,6 @@ describe('extractAggregationsPerQuery', () => {
});
});
describe('extractClickhouseQueryNames', () => {
it('collects names of clickhouse_sql queries, ignoring other envelope types', () => {
const request = requestWith([
{ type: 'clickhouse_sql', spec: { name: 'A', query: 'SELECT 1' } },
{
type: 'builder_query',
spec: { name: 'B', aggregations: [{ expression: 'count()' }] },
},
{ type: 'promql', spec: { name: 'P', query: 'up' } },
]);
expect(extractClickhouseQueryNames(request)).toStrictEqual(new Set(['A']));
});
it('returns an empty set for an undefined payload', () => {
expect(extractClickhouseQueryNames(undefined)).toStrictEqual(new Set());
});
});
describe('prepareScalarTables', () => {
it('builds keyed rows with group + aggregation columns (V1 getColName/getColId parity)', () => {
const [table] = prepareScalarTables({
@@ -213,115 +194,18 @@ describe('prepareScalarTables', () => {
expect(tables.map((t) => t.queryName)).toStrictEqual(['A', 'B']);
});
it('clickhouse_sql single value column uses the SQL alias over the legend', () => {
it('queries without aggregation metadata fall back to legend || queryName', () => {
const [table] = prepareScalarTables({
results: [
scalarResult(
[
{
name: 'current_availability',
queryName: 'A',
columnType: 'aggregation',
},
],
[],
),
],
legendMap: { A: 'Legend' },
requestPayload: requestWith([
{ type: 'clickhouse_sql', spec: { name: 'A', query: 'SELECT ...' } },
]),
});
// The query is clickhouse_sql, so the response column's real SQL alias is
// used for both header and key (a single legend can't be the column name).
expect(table.columns[0].name).toBe('current_availability');
expect(table.columns[0].id).toBe('current_availability');
});
it('non-clickhouse query without aggregation metadata falls back to legend || queryName', () => {
const [table] = prepareScalarTables({
results: [
// Formulas/promql carry placeholder names and are not clickhouse_sql,
// so they must not adopt the response column name.
scalarResult(
[{ name: '__result_0', queryName: 'A', columnType: 'aggregation' }],
[],
),
],
legendMap: { A: 'Legend' },
requestPayload: requestWith([
{ type: 'promql', spec: { name: 'A', query: 'up' } },
]),
requestPayload: requestWith([]),
});
expect(table.columns[0].name).toBe('Legend');
expect(table.columns[0].id).toBe('A');
});
it('clickhouse_sql query keeps each value column distinct (regression: all-"A" collapse)', () => {
const [table] = prepareScalarTables({
results: [
scalarResult(
[
{ name: 'service.name', queryName: 'A', columnType: 'group' },
{
name: 'current_availability',
queryName: 'A',
columnType: 'aggregation',
aggregationIndex: 0,
},
{
name: 'error_budget_remaining',
queryName: 'A',
columnType: 'aggregation',
aggregationIndex: 1,
},
{ name: 'budget_status', queryName: 'A', columnType: 'group' },
{
name: 'total_requests',
queryName: 'A',
columnType: 'aggregation',
aggregationIndex: 4,
},
],
[['kuja-api_gateway-service', 99.985, 0.985, 'Healthy ✅', 2181216]],
),
],
legendMap: { A: '' },
// A clickhouse_sql envelope contributes no aggregation metadata.
requestPayload: requestWith([
{
type: 'clickhouse_sql',
spec: { name: 'A', query: 'SELECT ...' },
},
]),
});
// Headers keep their real names instead of collapsing to "A".
expect(table.columns.map((col) => col.name)).toStrictEqual([
'service.name',
'current_availability',
'error_budget_remaining',
'budget_status',
'total_requests',
]);
// Ids are unique, so value columns don't overwrite each other in the row.
expect(table.columns.map((col) => col.id)).toStrictEqual([
'service.name',
'current_availability',
'error_budget_remaining',
'budget_status',
'total_requests',
]);
expect(table.rows).toStrictEqual([
{
data: {
'service.name': 'kuja-api_gateway-service',
current_availability: 99.985,
error_budget_remaining: 0.985,
budget_status: 'Healthy ✅',
total_requests: 2181216,
},
},
]);
});
});

View File

@@ -1,6 +1,5 @@
import type {
Querybuildertypesv5ColumnDescriptorDTO,
Querybuildertypesv5QueryEnvelopeClickHouseSQLDTO,
Querybuildertypesv5QueryRangeRequestDTO,
Querybuildertypesv5ScalarDataDTO,
} from 'api/generated/services/sigNoz.schemas';
@@ -45,43 +44,16 @@ export function extractAggregationsPerQuery(
return perQuery;
}
/**
* Names of the request's clickhouse_sql queries. These have no aggregation
* metadata, but their value columns carry the user's real SQL alias in the
* response `col.name` — so columns of these queries are named/keyed by that
* alias rather than collapsing onto the query name. Builder/formula/promql use
* placeholder names (`__result`/`__result_N`) and are excluded here.
*/
export function extractClickhouseQueryNames(
requestPayload: Querybuildertypesv5QueryRangeRequestDTO | undefined,
): Set<string> {
const names = new Set<string>();
(requestPayload?.compositeQuery?.queries ?? []).forEach((envelope) => {
if (envelope.type !== Querybuildertypesv5QueryTypeDTO.clickhouse_sql) {
return;
}
const spec = (envelope as Querybuildertypesv5QueryEnvelopeClickHouseSQLDTO)
.spec;
if (spec?.name) {
names.add(spec.name);
}
});
return names;
}
/**
* Column display name. Group columns keep their field name; aggregation
* columns resolve alias > legend > expression > queryName — with the legend
* skipped when the query has multiple aggregations, because one legend can't
* label several value columns. clickhouse_sql columns have no aggregation
* metadata, so their value columns are named by the real SQL alias the
* response carries in `col.name`. (Port of V1 `getColName`.)
* label several value columns. (Port of V1 `getColName`.)
*/
function getColName(
col: Querybuildertypesv5ColumnDescriptorDTO,
legendMap: Record<string, string>,
aggregationsPerQuery: AggregationsPerQuery,
clickhouseQueryNames: Set<string>,
): string {
if (col.columnType === 'group') {
return col.name;
@@ -102,13 +74,6 @@ function getColName(
return alias || expression || queryName;
}
// clickhouse_sql value columns carry their real SQL alias in col.name — use
// it so each value column keeps its own header instead of collapsing onto
// the query name. Formulas/promql use placeholder names, so they fall back
// to legend || queryName.
if (clickhouseQueryNames.has(queryName)) {
return col.name;
}
return legend || queryName;
}
@@ -120,23 +85,15 @@ function getColName(
function getColId(
col: Querybuildertypesv5ColumnDescriptorDTO,
aggregationsPerQuery: AggregationsPerQuery,
clickhouseQueryNames: Set<string>,
): string {
if (col.columnType === 'group') {
return col.name;
}
const queryName = col.queryName ?? '';
// clickhouse_sql value columns are keyed by their real SQL alias so multiple
// value columns stay unique instead of all collapsing onto the query name
// (which would overwrite every cell in the row with the last column's value).
if (clickhouseQueryNames.has(queryName)) {
return col.name;
}
const aggregations = aggregationsPerQuery[queryName];
const expression = aggregations?.[col.aggregationIndex ?? 0]?.expression || '';
if ((aggregations?.length || 0) > 1 && expression) {
return `${queryName}.${expression}`;
}
@@ -162,7 +119,6 @@ export function prepareScalarTables({
requestPayload,
}: PrepareScalarTablesArgs): PanelTable[] {
const aggregationsPerQuery = extractAggregationsPerQuery(requestPayload);
const clickhouseQueryNames = extractClickhouseQueryNames(requestPayload);
return results.map((scalarData) => {
if (!scalarData) {
@@ -176,10 +132,10 @@ export function prepareScalarTables({
const queryName = scalarData.columns?.[0]?.queryName ?? '';
const columns: PanelTableColumn[] = (scalarData.columns ?? []).map((col) => ({
name: getColName(col, legendMap, aggregationsPerQuery, clickhouseQueryNames),
name: getColName(col, legendMap, aggregationsPerQuery),
queryName: col.queryName ?? '',
isValueColumn: col.columnType === 'aggregation',
id: getColId(col, aggregationsPerQuery, clickhouseQueryNames),
id: getColId(col, aggregationsPerQuery),
}));
const rows = (scalarData.data ?? []).map((dataRow) => {

View File

@@ -1,55 +0,0 @@
import type { StateCreator } from 'zustand';
import type {
VariableSelection,
VariableSelectionMap,
} from '../../VariablesBar/selectionTypes';
import type { DashboardStore } from '../useDashboardStore';
/**
* Runtime variable selection — the values the user picks in the variable bar.
* Keyed by dashboardId → variable name. Frontend-only and persisted to
* localStorage (mirrored to the URL by the bar for shareable links); it is
* deliberately NOT part of the dashboard spec, so selecting a value never
* patches the dashboard.
*/
export interface VariableSelectionSlice {
variableValues: Record<string, VariableSelectionMap>;
setVariableValue: (
dashboardId: string,
name: string,
selection: VariableSelection,
) => void;
/** Bulk set (used to seed from URL/localStorage/defaults on load). */
setVariableValues: (dashboardId: string, values: VariableSelectionMap) => void;
}
export const createVariableSelectionSlice: StateCreator<
DashboardStore,
[['zustand/persist', unknown]],
[],
VariableSelectionSlice
> = (set, get) => ({
variableValues: {},
setVariableValue: (dashboardId, name, selection): void => {
const { variableValues } = get();
set({
variableValues: {
...variableValues,
[dashboardId]: { ...variableValues[dashboardId], [name]: selection },
},
});
},
setVariableValues: (dashboardId, values): void => {
const { variableValues } = get();
set({
variableValues: { ...variableValues, [dashboardId]: values },
});
},
});
/** Selector: the selection map for a dashboard (empty if none). */
export const selectVariableValues =
(dashboardId: string) =>
(state: DashboardStore): VariableSelectionMap =>
state.variableValues[dashboardId] ?? {};

View File

@@ -9,36 +9,25 @@ import {
createCollapseSlice,
type CollapseSlice,
} from './slices/collapseSlice';
import {
createVariableSelectionSlice,
type VariableSelectionSlice,
} from './slices/variableSelectionSlice';
export type DashboardStore = EditContextSlice &
CollapseSlice &
VariableSelectionSlice;
export type DashboardStore = EditContextSlice & CollapseSlice;
/**
* V2 dashboard session store. Holds cross-cutting client state only — never the
* dashboard spec (that stays in react-query via useGetDashboardV2). Slices:
* dashboard spec (that stays in react-query via useGetDashboardV2). Two slices:
* - edit-context: dashboardId / isEditable / refetch (set once, not persisted).
* - collapse: per-section open state (frontend-only, persisted to localStorage).
* - variable-selection: runtime variable values (frontend-only, persisted).
*/
export const useDashboardStore = create<DashboardStore>()(
persist(
(...a) => ({
...createEditContextSlice(...a),
...createCollapseSlice(...a),
...createVariableSelectionSlice(...a),
}),
{
name: '@signoz/dashboard-v2',
// Persist UI-only state (context incl. the refetch fn is transient).
partialize: (state) => ({
collapsed: state.collapsed,
variableValues: state.variableValues,
}),
// Persist only the collapse map — context (incl. the refetch fn) is transient.
partialize: (state) => ({ collapsed: state.collapsed }),
},
),
);

View File

@@ -1,20 +1,17 @@
.page {
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
flex: 1;
min-height: 0;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
padding: 0 8px;
gap: 8px;
height: 48px;
flex: none;
border-bottom: 1px solid var(--l2-border);
}
.headerLeft {

View File

@@ -1,12 +1,12 @@
import { useState } from 'react';
import { AnnouncementBanner } from '@signozhq/ui/announcement-banner';
import { Typography } from '@signozhq/ui/typography';
import { LayoutGrid } from '@signozhq/icons';
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
import DashboardsList from './components/DashboardsList/DashboardsList';
import DashboardsList from './components/DashboardsList';
import styles from './DashboardsListPageV2.module.scss';
import { BreadcrumbLink } from '@signozhq/ui/breadcrumb';
function DashboardsListPageV2(): JSX.Element {
const [showBanner, setShowBanner] = useState(true);
@@ -24,7 +24,8 @@ function DashboardsListPageV2(): JSX.Element {
)}
<div className={styles.header}>
<div className={styles.headerLeft}>
<BreadcrumbLink icon={<LayoutGrid size={14} />}>Dashboard</BreadcrumbLink>
<LayoutGrid size={14} className={styles.icon} />
<Typography.Text className={styles.text}>Dashboards</Typography.Text>
</div>
<HeaderRightSection
enableAnnouncements={false}

View File

@@ -1,21 +1,12 @@
import { useMutation } from 'react-query';
import { generatePath } from 'react-router-dom';
import { Popover } from 'antd';
import { Button } from '@signozhq/ui/button';
import { toast } from '@signozhq/ui/sonner';
import {
Copy,
Expand,
EllipsisVertical,
Link2,
SquareArrowOutUpRight,
} from '@signozhq/icons';
import { useCopyToClipboard } from 'react-use';
import { cloneDashboardV2 } from 'api/generated/services/dashboard';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { getAbsoluteUrl } from 'utils/basePath';
import { openInNewTab } from 'utils/navigation';
@@ -40,23 +31,6 @@ function ActionsPopover({
onView,
}: Props): JSX.Element {
const [, setCopy] = useCopyToClipboard();
const { safeNavigate } = useSafeNavigate();
const { showErrorModal } = useErrorModal();
// Clone keeps the source's name/panels/tags as a new unlocked dashboard owned
// by the caller; open the copy so it can be tweaked right away.
const { mutate: runClone, isLoading: isCloning } = useMutation({
mutationFn: () => cloneDashboardV2({ id: dashboardId }),
onSuccess: (response) => {
toast.success(`Duplicated "${dashboardName}"`);
safeNavigate(
generatePath(ROUTES.DASHBOARD, { dashboardId: response.data.id }),
);
},
onError: (error: APIError) => {
showErrorModal(error);
},
});
return (
<Popover
@@ -97,20 +71,6 @@ function ActionsPopover({
>
Copy Link
</Button>
<Button
color="secondary"
className={styles.menuItem}
prefix={<Copy size={14} />}
loading={isCloning}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
runClone();
}}
testId="dashboard-action-duplicate"
>
Duplicate
</Button>
<DeleteActionItem
dashboardId={dashboardId}
dashboardName={dashboardName}

View File

@@ -0,0 +1,164 @@
.content {
display: flex;
flex-direction: column;
gap: 14px;
}
.preview {
display: flex;
padding: 12px 14.634px;
flex-direction: column;
align-items: flex-start;
gap: 7.317px;
border-radius: 4px;
border: 0.915px solid var(--l1-border);
background: var(--l2-background);
}
.previewHeader {
display: flex;
gap: 10px;
align-items: center;
}
.previewIcon {
height: 14px;
width: 14px;
}
.previewTitle {
color: var(--l1-foreground);
font-family: Inter;
font-size: 12.805px;
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 18.293px;
letter-spacing: -0.064px;
}
.previewDetails {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.previewRow {
display: flex;
justify-content: space-between;
align-items: center;
}
.formattedTime {
display: inline-flex;
gap: 8px;
align-items: center;
color: var(--l2-foreground);
}
.formattedTimeText {
font-family: Inter;
font-size: 12.805px;
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 16.463px;
letter-spacing: -0.064px;
color: var(--l2-foreground);
}
.user {
display: flex;
align-items: center;
gap: 8px;
}
.userTag {
width: 12px;
height: 12px;
display: flex;
justify-content: center;
align-items: center;
color: var(--l2-foreground);
font-size: 8px;
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: normal;
letter-spacing: -0.05px;
border-radius: 12.805px;
background-color: var(--l1-background);
}
.userLabel {
color: var(--l2-foreground);
font-family: Inter;
font-size: 12.805px;
font-weight: var(--font-weight-normal);
line-height: 16.463px;
letter-spacing: -0.064px;
}
.action {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0px 0px 0px 14.634px;
}
.actionLeft {
display: flex;
gap: 10px;
align-items: center;
}
.connectionLine {
border-top: 1px dashed var(--l1-border);
min-width: 20px;
flex-grow: 1;
margin: 0px 8px;
}
.actionRight {
display: flex;
align-items: center;
}
.saveChanges {
display: flex;
width: 100%;
height: 32px;
padding: 8px 16px;
justify-content: center;
align-items: center;
flex-shrink: 0;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l1-border);
}
:global(.configureMetadataModalRoot) {
:global(.ant-modal-content) {
width: 500px;
flex-shrink: 0;
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--card);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
padding: 0px;
}
:global(.ant-modal-header) {
background: var(--card);
padding: 16px;
border-bottom: 1px solid var(--l1-border);
margin-bottom: 0px;
}
:global(.ant-modal-body) {
padding: 14px 16px;
}
:global(.ant-modal-footer) {
margin-top: 0px;
padding: 4px 16px 16px 16px;
}
}

View File

@@ -0,0 +1,218 @@
import { useEffect, useState } from 'react';
import { Button, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Switch } from '@signozhq/ui/switch';
import { CalendarClock, Check, Clock4 } from '@signozhq/icons';
import { get } from 'lodash-es';
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { useTimezone } from 'providers/Timezone';
import { lastUpdatedLabel, type DashboardListItem } from '../../utils';
import {
DynamicColumns,
useDashboardsListVisibleColumnsStore,
type DashboardDynamicColumns,
} from './useDynamicColumns';
import styles from './ConfigureMetadataModal.module.scss';
interface Props {
open: boolean;
previewDashboard: DashboardListItem | undefined;
onClose: () => void;
}
function ConfigureMetadataModal({
open,
previewDashboard,
onClose,
}: Props): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const storedColumns = useDashboardsListVisibleColumnsStore(
(s) => s.visibleColumns,
);
const setStoredColumns = useDashboardsListVisibleColumnsStore(
(s) => s.setVisibleColumns,
);
const [draftColumns, setDraftColumns] =
useState<DashboardDynamicColumns>(storedColumns);
useEffect(() => {
if (open) {
setDraftColumns(storedColumns);
}
}, [open, storedColumns]);
const handleSave = (): void => {
setStoredColumns(draftColumns);
onClose();
};
const previewImage = previewDashboard?.image || Base64Icons[0];
const previewName = previewDashboard?.spec?.display?.name;
const previewCreatedBy = previewDashboard?.createdBy;
const previewUpdatedBy = previewDashboard?.updatedBy;
const previewUpdatedAt = previewDashboard?.updatedAt;
const formattedCreatedAt = previewDashboard
? formatTimezoneAdjustedTimestamp(
get(previewDashboard, 'createdAt', '') as string,
DATE_TIME_FORMATS.DASH_DATETIME_UTC,
)
: '';
return (
<Modal
open={open}
onCancel={onClose}
title="Configure Metadata"
footer={
<Button
type="text"
icon={<Check size={14} />}
className={styles.saveChanges}
onClick={handleSave}
>
Save Changes
</Button>
}
rootClassName="configureMetadataModalRoot"
>
<div className={styles.content}>
<div className={styles.preview}>
<section className={styles.previewHeader}>
<img
src={previewImage}
alt="dashboard-image"
className={styles.previewIcon}
/>
<Typography.Text className={styles.previewTitle}>
{previewName}
</Typography.Text>
</section>
<section className={styles.previewDetails}>
<section className={styles.previewRow}>
{draftColumns.createdAt && (
<span className={styles.formattedTime}>
<CalendarClock size={14} />
<Typography.Text className={styles.formattedTimeText}>
{formattedCreatedAt}
</Typography.Text>
</span>
)}
{draftColumns.createdBy && (
<div className={styles.user}>
<Typography.Text className={styles.userTag}>
{previewCreatedBy?.substring(0, 1).toUpperCase()}
</Typography.Text>
<Typography.Text className={styles.userLabel}>
{previewCreatedBy}
</Typography.Text>
</div>
)}
</section>
<section className={styles.previewRow}>
{draftColumns.updatedAt && (
<span className={styles.formattedTime}>
<CalendarClock size={14} />
<Typography.Text className={styles.formattedTimeText}>
{lastUpdatedLabel(previewUpdatedAt)}
</Typography.Text>
</span>
)}
{draftColumns.updatedBy && (
<div className={styles.user}>
<Typography.Text className={styles.userTag}>
{previewUpdatedBy?.substring(0, 1).toUpperCase()}
</Typography.Text>
<Typography.Text className={styles.userLabel}>
{previewUpdatedBy}
</Typography.Text>
</div>
)}
</section>
</section>
</div>
<div className={styles.action}>
<div className={styles.actionLeft}>
<CalendarClock size={14} />
<Typography.Text>Created at</Typography.Text>
</div>
<div className={styles.connectionLine} />
<div className={styles.actionRight}>
<Switch
value
disabled
onChange={(check): void =>
setDraftColumns((prev) => ({
...prev,
[DynamicColumns.CREATED_AT]: check,
}))
}
/>
</div>
</div>
<div className={styles.action}>
<div className={styles.actionLeft}>
<CalendarClock size={14} />
<Typography.Text>Created by</Typography.Text>
</div>
<div className={styles.connectionLine} />
<div className={styles.actionRight}>
<Switch
value
disabled
onChange={(check): void =>
setDraftColumns((prev) => ({
...prev,
[DynamicColumns.CREATED_BY]: check,
}))
}
/>
</div>
</div>
<div className={styles.action}>
<div className={styles.actionLeft}>
<Clock4 size={14} />
<Typography.Text>Updated at</Typography.Text>
</div>
<div className={styles.connectionLine} />
<div className={styles.actionRight}>
<Switch
value={draftColumns.updatedAt}
onChange={(check): void =>
setDraftColumns((prev) => ({
...prev,
[DynamicColumns.UPDATED_AT]: check,
}))
}
/>
</div>
</div>
<div className={styles.action}>
<div className={styles.actionLeft}>
<Clock4 size={14} />
<Typography.Text>Updated by</Typography.Text>
</div>
<div className={styles.connectionLine} />
<div className={styles.actionRight}>
<Switch
value={draftColumns.updatedBy}
onChange={(check): void =>
setDraftColumns((prev) => ({
...prev,
[DynamicColumns.UPDATED_BY]: check,
}))
}
/>
</div>
</div>
</div>
</Modal>
);
}
export default ConfigureMetadataModal;

View File

@@ -0,0 +1,34 @@
.menuItem {
display: flex;
align-items: center;
gap: 8px;
}
.templatesItem {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
width: 100%;
}
.primaryButton {
padding: 6px 12px;
}
.textButton {
display: flex;
width: 153px;
align-items: center;
height: 32px;
padding: 6px 12px;
justify-content: center;
gap: 6px;
border-radius: 2px;
background: var(--primary-background);
color: var(--l1-foreground);
}
:global(.createDashboardMenuOverlay) {
width: 200px;
}

View File

@@ -0,0 +1,119 @@
import { useMemo } from 'react';
// eslint-disable-next-line signoz/no-antd-components -- TODO: migrate Dropdown to @signozhq/ui/dropdown-menu
import { Button, Dropdown, MenuProps } from 'antd';
import cx from 'classnames';
import logEvent from 'api/common/logEvent';
import {
ExternalLink,
Github,
LayoutGrid,
Plus,
Radius,
} from '@signozhq/icons';
import styles from './CreateDashboardDropdown.module.scss';
interface Props {
canCreate: boolean;
onCreate: () => void;
onImportJSON: () => void;
variant?: 'primary' | 'text';
}
const TEMPLATES_HREF =
'https://signoz.io/docs/dashboards/dashboard-templates/overview/';
function CreateDashboardDropdown({
canCreate,
onCreate,
onImportJSON,
variant = 'primary',
}: Props): JSX.Element {
const items: MenuProps['items'] = useMemo(() => {
const menuItems: MenuProps['items'] = [
{
key: 'import-json',
label: (
<div
className={styles.menuItem}
data-testid="import-json-menu-cta"
onClick={onImportJSON}
>
<Radius size={14} /> Import JSON
</div>
),
},
{
key: 'view-templates',
label: (
<a
href={TEMPLATES_HREF}
target="_blank"
rel="noopener noreferrer"
data-testid="view-templates-menu-cta"
>
<div className={styles.templatesItem}>
<div className={styles.menuItem}>
<Github size={14} /> View templates
</div>
<ExternalLink size={14} />
</div>
</a>
),
},
];
if (canCreate) {
menuItems.unshift({
key: 'create-dashboard',
label: (
<div
className={styles.menuItem}
data-testid="create-dashboard-menu-cta"
onClick={onCreate}
>
<LayoutGrid size={14} /> Create dashboard
</div>
),
});
}
return menuItems;
}, [canCreate, onCreate, onImportJSON]);
return (
<Dropdown
overlayClassName="createDashboardMenuOverlay"
menu={{ items }}
placement="bottomRight"
trigger={['click']}
>
{variant === 'primary' ? (
<Button
type="primary"
className={cx('periscope-btn primary', styles.primaryButton)}
icon={<Plus size={14} />}
data-testid="new-dashboard-cta"
onClick={(): void => {
logEvent('Dashboard List: New dashboard clicked', {});
}}
>
New dashboard
</Button>
) : (
<Button
type="text"
className={styles.textButton}
icon={<Plus size={14} />}
onClick={(): void => {
logEvent('Dashboard List: New dashboard clicked', {});
}}
>
New Dashboard
</Button>
)}
</Dropdown>
);
}
export default CreateDashboardDropdown;

View File

@@ -1,14 +1,9 @@
.row {
padding: 12px 16px 16px 16px;
border: 1px solid var(--l2-border);
border: 1px solid var(--l1-border);
border-top: none;
background: var(--l1-background);
cursor: pointer;
transition: background 0.12s;
}
.row:hover {
background: var(--l2-background);
cursor: pointer;
}
.titleWithAction {
@@ -62,40 +57,6 @@
justify-content: flex-end;
}
.favBtn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
flex: none;
border: none;
border-radius: 5px;
background: transparent;
color: transparent;
cursor: pointer;
transition:
background 0.12s,
color 0.12s;
}
.row:hover .favBtn {
color: var(--l3-foreground);
}
.favBtn:hover {
background: var(--l1-background);
color: var(--bg-amber-500);
}
.favBtnOn {
color: var(--bg-amber-500);
svg {
fill: currentColor;
}
}
.tags {
display: flex;
flex-wrap: wrap;

View File

@@ -1,8 +1,7 @@
import { Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Badge } from '@signozhq/ui/badge';
import { CalendarClock, Star } from '@signozhq/icons';
import cx from 'classnames';
import { CalendarClock } from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import { generatePath } from 'react-router-dom';
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
@@ -12,7 +11,6 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useTimezone } from 'providers/Timezone';
import { isModifierKeyPressed } from 'utils/app';
import { useDashboardViewsStore } from '../../store/useDashboardViewsStore';
import type { DashboardListItem } from '../../utils';
import { lastUpdatedLabel, tagsToStrings } from '../../utils';
import ActionsPopover from '../ActionsPopover/ActionsPopover';
@@ -37,12 +35,6 @@ function DashboardRow({
const { safeNavigate } = useSafeNavigate();
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const isFavorite = useDashboardViewsStore((s) =>
s.favorites.includes(dashboard.id),
);
const toggleFavorite = useDashboardViewsStore((s) => s.toggleFavorite);
const markViewed = useDashboardViewsStore((s) => s.markViewed);
const id = dashboard.id;
const name = dashboard.spec?.display?.name ?? '';
const image = dashboard.image || Base64Icons[0];
@@ -61,7 +53,6 @@ function DashboardRow({
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation();
markViewed(id);
safeNavigate(link, { newTab: isModifierKeyPressed(event) });
logEvent('Dashboard List: Clicked on dashboard', {
dashboardId: id,
@@ -69,11 +60,6 @@ function DashboardRow({
});
};
const onToggleFavorite = (event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation();
toggleFavorite(id);
};
return (
<div className={styles.row} onClick={onClickHandler}>
<div className={styles.titleWithAction}>
@@ -112,17 +98,6 @@ function DashboardRow({
)}
</div>
<button
type="button"
className={cx(styles.favBtn, { [styles.favBtnOn]: isFavorite })}
aria-label={isFavorite ? 'Remove from favorites' : 'Add to favorites'}
title={isFavorite ? 'Remove from favorites' : 'Add to favorites'}
data-testid={`dashboard-favorite-${index}`}
onClick={onToggleFavorite}
>
<Star size={14} />
</button>
{canAct && (
<ActionsPopover
link={link}

View File

@@ -1,32 +0,0 @@
import { Typography } from '@signozhq/ui/typography';
import NewDashboardButton from './NewDashboardButton';
import styles from './DashboardsList.module.scss';
interface Props {
label: string;
count: number;
canCreate: boolean;
onCreate: () => void;
}
function CommandHeader({
label,
count,
canCreate,
onCreate,
}: Props): JSX.Element {
return (
<div className={styles.commandHeader}>
<div className={styles.headingBlock}>
<Typography.Title className={styles.title}>{label}</Typography.Title>
<span className={styles.countPill}>{count}</span>
</div>
<div className={styles.grow} />
{canCreate && <NewDashboardButton onClick={onCreate} />}
</div>
);
}
export default CommandHeader;

View File

@@ -1,43 +1,14 @@
.layout {
.container {
margin-top: 30px;
margin-bottom: 30px;
display: flex;
align-items: stretch;
justify-content: center;
width: 100%;
flex: 1;
min-height: 0;
}
.main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
min-height: 0;
// Deepest layer — the results canvas, so the lighter header zone and the
// row cards read with clear contrast (matches the design's list surface).
background: var(--l1-background);
}
.mainScroll {
flex: 1;
min-height: 0;
overflow-y: auto;
}
.headerZone {
display: flex;
flex-direction: column;
gap: 14px;
padding: 20px 24px;
background: var(--l2-background);
border-bottom: 1px solid var(--l2-border);
}
.emptyWrap {
padding: 24px;
}
.viewContent {
width: 100%;
width: calc(100% - 30px);
max-width: 836px;
:global(.ant-table-wrapper) :global(.ant-table-cell) {
padding: 0 !important;
@@ -45,6 +16,14 @@
background: var(--l1-background) !important;
}
:global(.ant-table-wrapper)
:global(.ant-table-tbody)
:global(.ant-table-row)
:global(.ant-table-cell)
> div {
// Row content is the only child of the td; it carries the borders.
}
:global(.ant-table-wrapper)
:global(.ant-table-tbody)
:global(.ant-table-row:last-child)
@@ -76,43 +55,19 @@
}
}
.commandHeader {
.titleContainer {
display: flex;
align-items: center;
gap: 12px;
}
.headingBlock {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.grow {
flex: 1;
}
.countPill {
padding: 2px 9px;
border-radius: 999px;
background: var(--l2-background);
border: 1px solid var(--l2-border);
color: var(--l2-foreground);
font-size: var(--font-size-xs);
font-variant-numeric: tabular-nums;
flex-direction: column;
gap: 4px;
}
.title {
color: var(--l1-foreground);
font-size: var(--font-size-lg);
font-style: normal;
font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-normal);
line-height: 28px;
letter-spacing: -0.09px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.subtitle {
@@ -125,16 +80,17 @@
}
.integrationsContainer {
width: 100%;
margin: 16px 0;
}
.integrationsContent {
max-width: 100%;
width: 100%;
// The shared request banner ships a 12px margin; drop it so the banner's
// left edge lines up with the heading and filters above/below it.
:global(.request-entity-container) {
margin: 0;
}
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
margin: 16px 0;
}

View File

@@ -1,45 +1,55 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { generatePath } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Typography } from '@signozhq/ui/typography';
import { AxiosError } from 'axios';
import logEvent from 'api/common/logEvent';
import { useListDashboardsV2 } from 'api/generated/services/dashboard';
import {
createDashboardV2,
useListDashboardsV2,
} from 'api/generated/services/dashboard';
import {
DashboardtypesListOrderDTO,
DashboardtypesListSortDTO,
} from 'api/generated/services/sigNoz.schemas';
import ROUTES from 'constants/routes';
import { RequestDashboardBtn } from 'container/ListOfDashboard/RequestDashboardBtn';
import useComponentPermission from 'hooks/useComponentPermission';
import { toast } from '@signozhq/ui/sonner';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { toAPIError } from 'utils/errorUtils';
import { combineQueries } from '../../filterQuery';
import { useActiveView } from '../../hooks/useActiveView';
import { useDashboardFilters } from '../../hooks/useDashboardFilters';
import {
usePage,
useSearch,
useSortColumn,
useSortOrder,
} from '../../hooks/useDashboardsListQueryParams';
import { useDashboardViewsStore } from '../../store/useDashboardViewsStore';
import { useDashboardsListVisibleColumnsStore } from '../../store/useVisibleColumnsStore';
import type { UpdatedWindow } from '../../types';
import type { DashboardListItem } from '../../utils';
import { applyClientView } from '../../views';
import type { CreatorOption } from '../FilterZone/FilterChips';
import FilterZone from '../FilterZone/FilterZone';
import NewDashboardModal from '../NewDashboardModal/NewDashboardModal';
import StatusBar from '../StatusBar/StatusBar';
import ViewsRail from '../ViewsRail/ViewsRail';
import CommandHeader from './CommandHeader';
import DashboardsResults from './DashboardsResults';
import WorkspaceEmptyState from './WorkspaceEmptyState';
import ConfigureMetadataModal from '../ConfigureMetadataModal/ConfigureMetadataModal';
import { useDashboardsListVisibleColumnsStore } from '../ConfigureMetadataModal/useDynamicColumns';
import CreateDashboardDropdown from '../CreateDashboardDropdown/CreateDashboardDropdown';
import ImportJSONModal from '../ImportJSONModal/ImportJSONModal';
import ListHeader from '../ListHeader/ListHeader';
import EmptyState from '../states/EmptyState/EmptyState';
import ErrorState from '../states/ErrorState/ErrorState';
import LoadingState from '../states/LoadingState/LoadingState';
import NoResultsState from '../states/NoResultsState/NoResultsState';
import SearchBar from '../SearchBar/SearchBar';
import DashboardsListContent from './DashboardsListContent';
import styles from './DashboardsList.module.scss';
const PAGE_SIZE = 20;
// Favorites / recently-viewed are filtered client-side (no server id filter), so
// we pull a single large page and constrain it in-memory.
const CLIENT_VIEW_LIMIT = 200;
function DashboardsList(): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { t } = useTranslation('dashboard');
const { showErrorModal } = useErrorModal();
const { isCloudUser } = useGetTenantLicense();
const { user } = useAppContext();
@@ -48,100 +58,38 @@ function DashboardsList(): JSX.Element {
user.role,
);
const {
filters,
query,
isEmpty: filtersEmpty,
setSearch,
setCreatedBy,
setUpdated,
applyFilters,
clearAll,
} = useDashboardFilters();
const [searchString, setSearchString] = useSearch();
const [sortColumn, setSortColumn] = useSortColumn();
const [sortOrder, setSortOrder] = useSortOrder();
const [page, setPage] = usePage();
const {
activeViewId,
builtinViews,
customViews,
isCustomActive,
isModified,
viewQuery,
clientView,
selectView,
saveView,
saveActiveView,
resetView,
removeView,
} = useActiveView({ filters, applyFilters, userEmail: user.email });
const [searchInput, setSearchInput] = useState(searchString);
const railCollapsed = useDashboardViewsStore((s) => s.railCollapsed);
const setRailCollapsed = useDashboardViewsStore((s) => s.setRailCollapsed);
const favorites = useDashboardViewsStore((s) => s.favorites);
const recent = useDashboardViewsStore((s) => s.recent);
// Keep the local input in sync with external searchString changes
// (browser back/forward, deep link). User typing only mutates
// searchInput, so this won't fight with in-flight edits.
useEffect(() => {
setSearchInput(searchString);
}, [searchString]);
// Any filter change resets to the first page so the user isn't stranded on a
// now-out-of-range offset.
const handleSearchChange = useCallback(
(value: string): void => {
setSearch(value);
void setPage(1);
},
[setSearch, setPage],
);
const handleCreatedByChange = useCallback(
(emails: string[]): void => {
setCreatedBy(emails);
void setPage(1);
},
[setCreatedBy, setPage],
);
const handleUpdatedChange = useCallback(
(window: UpdatedWindow): void => {
setUpdated(window);
void setPage(1);
},
[setUpdated, setPage],
);
const handleClearAll = useCallback((): void => {
clearAll();
const handleSubmitSearch = useCallback((): void => {
const next = searchInput.trim();
if (next === searchString) {
return;
}
void setSearchString(next);
void setPage(1);
}, [clearAll, setPage]);
// View actions that change the result set reset pagination too.
const handleSelectView = useCallback(
(id: string): void => {
selectView(id);
void setPage(1);
},
[selectView, setPage],
);
const handleResetView = useCallback((): void => {
resetView();
void setPage(1);
}, [resetView, setPage]);
const handleRemoveView = useCallback(
(id: string): void => {
removeView(id);
void setPage(1);
},
[removeView, setPage],
);
const toggleRail = useCallback((): void => {
setRailCollapsed(!railCollapsed);
}, [setRailCollapsed, railCollapsed]);
}, [searchInput, searchString, setSearchString, setPage]);
const listParams = useMemo(
() => ({
query: combineQueries(viewQuery, query) || undefined,
query: searchString.trim() || undefined,
sort: sortColumn,
order: sortOrder,
limit: clientView ? CLIENT_VIEW_LIMIT : PAGE_SIZE,
offset: clientView ? 0 : (page - 1) * PAGE_SIZE,
limit: PAGE_SIZE,
offset: (page - 1) * PAGE_SIZE,
}),
[viewQuery, query, sortColumn, sortOrder, page, clientView],
[searchString, sortColumn, sortOrder, page],
);
const {
@@ -159,49 +107,52 @@ function DashboardsList(): JSX.Element {
const errorHttpStatus = apiError?.getHttpStatusCode();
const errorMessage = apiError?.getErrorMessage();
const rawDashboards = useMemo<DashboardListItem[]>(
const dashboards = useMemo<DashboardListItem[]>(
() => response?.data?.dashboards ?? [],
[response],
);
const total = response?.data?.total ?? 0;
// Favorites / recently-viewed constrain the fetched rows by a client-side id
// set; all other views are already constrained server-side.
const dashboards = useMemo<DashboardListItem[]>(
() =>
clientView
? applyClientView(rawDashboards, activeViewId, favorites, recent)
: rawDashboards,
[clientView, rawDashboards, activeViewId, favorites, recent],
);
const total = clientView ? dashboards.length : (response?.data?.total ?? 0);
// Creator filter options: distinct authors on the loaded page plus the
// current user (so "me" is always selectable). Page-scoped until a members
// source backs this.
const creatorOptions = useMemo<CreatorOption[]>(() => {
const emails = new Set<string>();
if (user.email) {
emails.add(user.email);
}
rawDashboards.forEach((d) => {
if (d.createdBy) {
emails.add(d.createdBy);
}
});
return [...emails].sort().map((email) => ({
email,
label: email === user.email ? `${email} (me)` : email,
}));
}, [rawDashboards, user.email]);
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [isImportOpen, setIsImportOpen] = useState(false);
const [isConfigureOpen, setIsConfigureOpen] = useState(false);
const visibleColumns = useDashboardsListVisibleColumnsStore(
(s) => s.visibleColumns,
);
const openCreate = useCallback((): void => {
logEvent('Dashboard List: New dashboard clicked', {});
setIsCreateOpen(true);
const [creating, setCreating] = useState(false);
const handleCreateNew = useCallback(async (): Promise<void> => {
try {
logEvent('Dashboard List: Create dashboard clicked', {});
setCreating(true);
const created = await createDashboardV2({
schemaVersion: 'v6',
// Backend requires `name` (immutable, server-side identifier);
// asking it to generate one keeps the UI's "new dashboard" flow.
generateName: true,
tags: null,
spec: {
display: { name: t('new_dashboard_title', { ns: 'dashboard' }) },
layouts: [],
panels: {},
variables: [],
// TODO(@AshwinBhatkal): duration and refresh interval need to be integrated
},
});
safeNavigate(
generatePath(ROUTES.DASHBOARD, { dashboardId: created.data.id }),
);
} catch (e) {
showErrorModal(e as APIError);
toast.error((e as AxiosError).toString() || 'Failed to create dashboard');
} finally {
setCreating(false);
}
}, [safeNavigate, showErrorModal, t]);
const handleImportToggle = useCallback((): void => {
logEvent('Dashboard List V2: Import JSON clicked', {});
setIsImportOpen((s) => !s);
}, []);
const onSortChange = useCallback(
@@ -229,109 +180,102 @@ function DashboardsList(): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading]);
const activeLabel =
customViews.find((v) => v.id === activeViewId)?.name ??
builtinViews.find((v) => v.id === activeViewId)?.label ??
'Dashboards';
// The workspace-empty CTA ("create your first dashboard") belongs only to the
// unfiltered All view; every other view's zero result is a no-results state.
const showWorkspaceEmpty =
!error &&
dashboards.length === 0 &&
activeViewId === 'all' &&
filtersEmpty &&
page === 1;
const isWorkspaceEmpty = showWorkspaceEmpty && !isLoading;
return (
<div className={styles.layout}>
<ViewsRail
activeViewId={activeViewId}
builtinViews={builtinViews}
customViews={customViews}
isCustomActive={isCustomActive}
isModified={isModified}
collapsed={railCollapsed}
onSelect={handleSelectView}
onSave={saveView}
onSaveChanges={saveActiveView}
onReset={handleResetView}
onClearFilters={handleClearAll}
onDelete={handleRemoveView}
/>
<div className={styles.main}>
<div className={styles.mainScroll}>
{isWorkspaceEmpty ? (
<WorkspaceEmptyState
canCreate={canCreateNewDashboard}
onCreate={openCreate}
/>
) : (
<>
<div className={styles.headerZone}>
<CommandHeader
label={activeLabel}
count={total}
canCreate={canCreateNewDashboard}
onCreate={openCreate}
/>
<FilterZone
search={filters.search}
createdBy={filters.createdBy}
updated={filters.updated}
creatorOptions={creatorOptions}
isEmpty={filtersEmpty}
onSearchChange={handleSearchChange}
onCreatedByChange={handleCreatedByChange}
onUpdatedChange={handleUpdatedChange}
onClearAll={handleClearAll}
/>
<div className={styles.container}>
<div className={styles.viewContent}>
<div className={styles.titleContainer}>
<Typography.Title className={styles.title}>Dashboards</Typography.Title>
<Typography.Text className={styles.subtitle}>
Create and manage dashboards for your workspace.
</Typography.Text>
{isCloudUser && (
<div className={styles.integrationsContainer}>
<div className={styles.integrationsContent}>
<RequestDashboardBtn />
</div>
<div className={styles.viewContent}>
<DashboardsResults
isLoading={isLoading}
hasError={!!error}
isCloudUser={!!isCloudUser}
onRetry={(): void => {
refetch();
}}
errorHttpStatus={errorHttpStatus}
errorMessage={errorMessage}
dashboards={dashboards}
activeViewId={activeViewId}
searchValue={filters.search}
hasFilters={!filtersEmpty}
</div>
)}
</div>
{isLoading ? (
<LoadingState />
) : !error && dashboards.length === 0 && !searchString && page === 1 ? (
<EmptyState
createDropdown={
canCreateNewDashboard ? (
<CreateDashboardDropdown
canCreate={!!canCreateNewDashboard}
onCreate={handleCreateNew}
onImportJSON={handleImportToggle}
variant="text"
/>
) : null
}
/>
) : (
<>
<div className={styles.toolbar}>
<SearchBar
value={searchInput}
onChange={setSearchInput}
onSubmit={handleSubmitSearch}
/>
{canCreateNewDashboard && (
<CreateDashboardDropdown
canCreate={!!canCreateNewDashboard}
onCreate={handleCreateNew}
onImportJSON={handleImportToggle}
/>
)}
</div>
{error ? (
<ErrorState
isCloudUser={!!isCloudUser}
onRetry={(): void => {
refetch();
}}
httpStatus={errorHttpStatus}
errorMessage={errorMessage}
/>
) : dashboards.length === 0 ? (
<NoResultsState searchString={searchInput} />
) : (
<>
<ListHeader
sortColumn={sortColumn}
onSortChange={onSortChange}
sortOrder={sortOrder}
onOrderChange={onOrderChange}
onConfigureMetadata={(): void => setIsConfigureOpen(true)}
/>
<DashboardsListContent
dashboards={dashboards}
page={page}
pageSize={clientView ? CLIENT_VIEW_LIMIT : PAGE_SIZE}
pageSize={PAGE_SIZE}
total={total}
onPageChange={setPage}
canAct={!!action}
showUpdatedAt={visibleColumns.updatedAt}
showUpdatedBy={visibleColumns.updatedBy}
loading={isFetching}
loading={creating || isFetching}
/>
</div>
</>
)}
</div>
<StatusBar
collapsed={railCollapsed}
onToggleCollapse={toggleRail}
count={dashboards.length}
total={total}
</>
)}
</>
)}
<ImportJSONModal
open={isImportOpen}
onClose={(): void => setIsImportOpen(false)}
/>
<ConfigureMetadataModal
open={isConfigureOpen}
previewDashboard={dashboards[0]}
onClose={(): void => setIsConfigureOpen(false)}
/>
</div>
<NewDashboardModal
open={isCreateOpen}
onClose={(): void => setIsCreateOpen(false)}
/>
</div>
);
}

View File

@@ -1,103 +0,0 @@
import {
DashboardtypesListOrderDTO,
DashboardtypesListSortDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { DashboardListItem } from '../../utils';
import { noResultsCopy } from '../../views';
import ListHeader from '../ListHeader/ListHeader';
import ErrorState from '../states/ErrorState/ErrorState';
import LoadingState from '../states/LoadingState/LoadingState';
import NoResultsState from '../states/NoResultsState/NoResultsState';
import DashboardsListContent from './DashboardsListContent';
interface Props {
isLoading: boolean;
hasError: boolean;
isCloudUser: boolean;
onRetry: () => void;
errorHttpStatus?: number;
errorMessage?: string;
dashboards: DashboardListItem[];
activeViewId: string;
searchValue: string;
hasFilters: boolean;
sortColumn: DashboardtypesListSortDTO;
onSortChange: (column: DashboardtypesListSortDTO) => void;
sortOrder: DashboardtypesListOrderDTO;
onOrderChange: (order: DashboardtypesListOrderDTO) => void;
page: number;
pageSize: number;
total: number;
onPageChange: (page: number) => void;
canAct: boolean;
showUpdatedAt: boolean;
showUpdatedBy: boolean;
loading: boolean;
}
function DashboardsResults({
isLoading,
hasError,
isCloudUser,
onRetry,
errorHttpStatus,
errorMessage,
dashboards,
activeViewId,
searchValue,
hasFilters,
sortColumn,
onSortChange,
sortOrder,
onOrderChange,
page,
pageSize,
total,
onPageChange,
canAct,
showUpdatedAt,
showUpdatedBy,
loading,
}: Props): JSX.Element {
if (isLoading) {
return <LoadingState />;
}
if (hasError) {
return (
<ErrorState
isCloudUser={isCloudUser}
onRetry={onRetry}
httpStatus={errorHttpStatus}
errorMessage={errorMessage}
/>
);
}
if (dashboards.length === 0) {
const copy = noResultsCopy(activeViewId, searchValue, hasFilters);
return <NoResultsState title={copy.title} description={copy.description} />;
}
return (
<>
<ListHeader
sortColumn={sortColumn}
onSortChange={onSortChange}
sortOrder={sortOrder}
onOrderChange={onOrderChange}
/>
<DashboardsListContent
dashboards={dashboards}
page={page}
pageSize={pageSize}
total={total}
onPageChange={onPageChange}
canAct={canAct}
showUpdatedAt={showUpdatedAt}
showUpdatedBy={showUpdatedBy}
loading={loading}
/>
</>
);
}
export default DashboardsResults;

View File

@@ -1,22 +0,0 @@
import { Button } from '@signozhq/ui/button';
import { Plus } from '@signozhq/icons';
interface Props {
onClick: () => void;
}
function NewDashboardButton({ onClick }: Props): JSX.Element {
return (
<Button
variant="solid"
color="primary"
prefix={<Plus size={14} />}
onClick={onClick}
testId="new-dashboard-cta"
>
New dashboard
</Button>
);
}
export default NewDashboardButton;

View File

@@ -1,23 +0,0 @@
import EmptyState from '../states/EmptyState/EmptyState';
import NewDashboardButton from './NewDashboardButton';
import styles from './DashboardsList.module.scss';
interface Props {
canCreate: boolean;
onCreate: () => void;
}
function WorkspaceEmptyState({ canCreate, onCreate }: Props): JSX.Element {
return (
<div className={styles.emptyWrap}>
<EmptyState
createDropdown={
canCreate ? <NewDashboardButton onClick={onCreate} /> : null
}
/>
</div>
);
}
export default WorkspaceEmptyState;

View File

@@ -0,0 +1,3 @@
import DashboardsList from './DashboardsList';
export default DashboardsList;

View File

@@ -1,129 +0,0 @@
import { useMemo } from 'react';
import { Button } from '@signozhq/ui/button';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import { CalendarClock, ChevronDown, User } from '@signozhq/icons';
import cx from 'classnames';
import type { UpdatedWindow } from '../../types';
import styles from './FilterZone.module.scss';
export interface CreatorOption {
email: string;
label: string;
}
const UPDATED_LABELS: Record<UpdatedWindow, string> = {
any: 'Any time',
today: 'Today',
'7d': 'Last 7 days',
'30d': 'Last 30 days',
};
const UPDATED_WINDOWS: UpdatedWindow[] = ['any', 'today', '7d', '30d'];
interface Props {
createdBy: string[];
updated: UpdatedWindow;
creatorOptions: CreatorOption[];
onCreatedByChange: (emails: string[]) => void;
onUpdatedChange: (window: UpdatedWindow) => void;
}
function FilterChips({
createdBy,
updated,
creatorOptions,
onCreatedByChange,
onUpdatedChange,
}: Props): JSX.Element {
const createdByLabel = useMemo((): string => {
if (createdBy.length === 0) {
return 'Anyone';
}
if (createdBy.length === 1) {
const match = creatorOptions.find((o) => o.email === createdBy[0]);
return match?.label ?? createdBy[0];
}
return `${createdBy.length} people`;
}, [createdBy, creatorOptions]);
const createdByItems = useMemo<MenuItem[]>(() => {
const items: MenuItem[] = creatorOptions.map((option) => ({
type: 'checkbox',
key: option.email,
label: option.label,
checked: createdBy.includes(option.email),
onCheckedChange: (checked: boolean): void =>
onCreatedByChange(
checked
? [...createdBy, option.email]
: createdBy.filter((e) => e !== option.email),
),
}));
if (createdBy.length > 0) {
items.push({ type: 'divider', key: 'sep' });
items.push({
key: 'clear',
label: 'Clear selection',
onClick: (): void => onCreatedByChange([]),
});
}
return items;
}, [creatorOptions, createdBy, onCreatedByChange]);
const updatedItems = useMemo<MenuItem[]>(
() => [
{
type: 'radio-group',
value: updated,
onChange: (value: string): void => onUpdatedChange(value as UpdatedWindow),
children: UPDATED_WINDOWS.map((window) => ({
type: 'radio',
key: window,
value: window,
label: UPDATED_LABELS[window],
})),
},
],
[updated, onUpdatedChange],
);
return (
<div className={styles.chips}>
<DropdownMenuSimple menu={{ items: createdByItems }} align="start">
<Button
variant="outlined"
color="secondary"
size="sm"
prefix={<User size={12} />}
suffix={<ChevronDown size={12} />}
className={cx(styles.chip, {
[styles.chipActive]: createdBy.length > 0,
})}
testId="dashboards-filter-created-by"
>
Created by: {createdByLabel}
</Button>
</DropdownMenuSimple>
<DropdownMenuSimple menu={{ items: updatedItems }} align="start">
<Button
variant="outlined"
color="secondary"
size="sm"
prefix={<CalendarClock size={12} />}
suffix={<ChevronDown size={12} />}
className={cx(styles.chip, {
[styles.chipActive]: updated !== 'any',
})}
testId="dashboards-filter-updated"
>
Updated: {UPDATED_LABELS[updated]}
</Button>
</DropdownMenuSimple>
</div>
);
}
export default FilterChips;

View File

@@ -1,50 +0,0 @@
.filterZone {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}
.searchRow {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.searchInput {
flex: 1;
min-width: 0;
}
.filtersRow {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.filtersLabel {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--l2-foreground);
margin-right: 2px;
}
.chips {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.chip {
font-size: var(--font-size-sm);
}
.chipActive {
border-color: var(--primary-background) !important;
color: var(--l1-foreground) !important;
}

View File

@@ -1,94 +0,0 @@
import { type ReactNode, useCallback, useEffect, useState } from 'react';
import { Button } from '@signozhq/ui/button';
import { X } from '@signozhq/icons';
import type { UpdatedWindow } from '../../types';
import SearchBar from '../SearchBar/SearchBar';
import FilterChips, { type CreatorOption } from './FilterChips';
import styles from './FilterZone.module.scss';
interface Props {
search: string;
createdBy: string[];
updated: UpdatedWindow;
creatorOptions: CreatorOption[];
isEmpty: boolean;
onSearchChange: (value: string) => void;
onCreatedByChange: (emails: string[]) => void;
onUpdatedChange: (window: UpdatedWindow) => void;
onClearAll: () => void;
// Rendered at the end of the search row (e.g. the New Dashboard action).
rightSlot?: ReactNode;
}
// The filter command zone: name search + structured chips (created-by, updated)
// + clear-all. Search is committed on submit/blur (matching the prior bar);
// chips apply immediately.
function FilterZone({
search,
createdBy,
updated,
creatorOptions,
isEmpty,
onSearchChange,
onCreatedByChange,
onUpdatedChange,
onClearAll,
rightSlot,
}: Props): JSX.Element {
const [searchInput, setSearchInput] = useState(search);
// Keep the local input in sync with external search changes (applying a view,
// clear-all, back/forward). User typing only mutates the local copy.
useEffect(() => {
setSearchInput(search);
}, [search]);
const handleSubmit = useCallback((): void => {
const next = searchInput.trim();
if (next !== search) {
onSearchChange(next);
}
}, [searchInput, search, onSearchChange]);
return (
<div className={styles.filterZone}>
<div className={styles.searchRow}>
<div className={styles.searchInput}>
<SearchBar
value={searchInput}
placeholder="Search dashboards by name"
onChange={setSearchInput}
onSubmit={handleSubmit}
/>
</div>
{rightSlot}
</div>
<div className={styles.filtersRow}>
<span className={styles.filtersLabel}>Filters</span>
<FilterChips
createdBy={createdBy}
updated={updated}
creatorOptions={creatorOptions}
onCreatedByChange={onCreatedByChange}
onUpdatedChange={onUpdatedChange}
/>
{!isEmpty && (
<Button
variant="ghost"
color="secondary"
size="sm"
prefix={<X size={12} />}
onClick={onClearAll}
testId="dashboards-filter-clear"
>
Clear
</Button>
)}
</div>
</div>
);
}
export default FilterZone;

View File

@@ -0,0 +1,73 @@
.contentContainer {
display: flex;
flex-direction: column;
}
.contentHeader {
padding: 16px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--l1-border);
}
.footer {
display: flex;
flex-direction: column;
gap: 8px;
}
.jsonError {
display: flex;
align-items: center;
gap: 8px;
}
.errorText {
color: var(--warning-background);
}
.actions {
display: flex;
justify-content: space-between;
align-items: center;
}
:global(.importJsonModalWrapper) {
:global(.ant-modal-content) {
border-radius: 4px;
border: 1px solid var(--l1-border);
background: linear-gradient(
139deg,
color-mix(in srgb, var(--card) 80%, transparent) 0%,
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
padding: 0;
}
:global(.margin) {
background: linear-gradient(
139deg,
color-mix(in srgb, var(--card) 80%, transparent) 0%,
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
);
backdrop-filter: blur(20px);
}
:global(.view-lines) {
background: linear-gradient(
139deg,
color-mix(in srgb, var(--card) 80%, transparent) 0%,
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
);
backdrop-filter: blur(20px);
}
:global(.ant-modal-footer) {
margin-top: 0;
padding: 16px;
border-top: 1px solid var(--l1-border);
}
}

View File

@@ -0,0 +1,223 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { red } from '@ant-design/colors';
import MEditor, { Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Button, Flex, Modal, Upload, UploadProps } from 'antd';
import { toast } from '@signozhq/ui/sonner';
import { Typography } from '@signozhq/ui/typography';
import {
CircleAlert,
ExternalLink,
Github,
MonitorDot,
MoveRight,
Sparkles,
} from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import { createDashboardV2 } from 'api/generated/services/dashboard';
import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import sampleDashboard from './sampleDashboard.json';
import styles from './ImportJSONModal.module.scss';
import { normalizeToPostable } from './ImportJSONModalUtils';
interface Props {
open: boolean;
onClose: () => void;
}
function ImportJSONModal({ open, onClose }: Props): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { t } = useTranslation(['dashboard', 'common']);
const [isUploadError, setIsUploadError] = useState(false);
const [isCreateError, setIsCreateError] = useState(false);
const [isCreating, setIsCreating] = useState(false);
const [editorValue, setEditorValue] = useState('');
const { showErrorModal } = useErrorModal();
const isDarkMode = useIsDarkMode();
const handleUpload: UploadProps['onChange'] = (info) => {
const lastFile = info.fileList[info.fileList.length - 1];
if (!lastFile?.originFileObj) {
return;
}
const reader = new FileReader();
reader.onload = (event): void => {
try {
const target = event.target?.result;
if (!target) {
return;
}
const parsed = JSON.parse(target.toString());
setEditorValue(JSON.stringify(parsed, null, 2));
setIsUploadError(false);
} catch {
setIsUploadError(true);
}
};
reader.readAsText(lastFile.originFileObj);
};
const handleImport = async (): Promise<void> => {
try {
setIsCreating(true);
logEvent('Dashboard List V2: Import and next clicked', {});
const parsed = JSON.parse(editorValue) as Record<string, unknown>;
const payload = normalizeToPostable(parsed);
const response = await createDashboardV2(payload);
safeNavigate(
generatePath(ROUTES.DASHBOARD, { dashboardId: response.data.id }),
);
logEvent('Dashboard List V2: New dashboard imported successfully', {
dashboardId: response.data?.id,
});
} catch (error) {
showErrorModal(error as APIError);
setIsCreateError(true);
toast.error(
error instanceof Error ? error.message : t('error_loading_json'),
);
} finally {
setIsCreating(false);
}
};
const handleClose = (): void => {
setIsUploadError(false);
setIsCreateError(false);
onClose();
};
const setEditorTheme = (monaco: Monaco): void => {
monaco.editor.defineTheme('my-theme', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'string.key.json', foreground: Color.BG_VANILLA_400 },
{ token: 'string.value.json', foreground: Color.BG_ROBIN_400 },
],
colors: { 'editor.background': Color.BG_INK_300 },
});
};
const renderError = (msg: string): JSX.Element => (
<div className={styles.jsonError}>
<CircleAlert size="md" color={red[7]} />
<Typography className={styles.errorText}>{msg}</Typography>
</div>
);
return (
<Modal
wrapClassName="importJsonModalWrapper"
open={open}
centered
closable
keyboard
maskClosable
onCancel={handleClose}
destroyOnClose
width="60vw"
footer={
<div className={styles.footer}>
{isCreateError && renderError(t('error_loading_json'))}
{isUploadError && renderError(t('error_upload_json'))}
<div className={styles.actions}>
<Flex gap="small">
<Upload
accept=".json"
showUploadList={false}
multiple={false}
onChange={handleUpload}
beforeUpload={(): boolean => false}
action="none"
>
<Button
type="default"
className="periscope-btn"
icon={<MonitorDot size={14} />}
onClick={(): void => {
logEvent('Dashboard List V2: Upload JSON file clicked', {});
}}
>
{t('upload_json_file')}
</Button>
</Upload>
<Button
type="default"
className="periscope-btn"
icon={<Sparkles size={14} />}
onClick={(): void => {
setEditorValue(JSON.stringify(sampleDashboard, null, 2));
setIsUploadError(false);
logEvent('Dashboard List V2: Load sample clicked', {});
}}
>
Load sample
</Button>
<a
href="https://signoz.io/docs/dashboards/dashboard-templates/overview/"
target="_blank"
rel="noopener noreferrer"
>
<Button
type="default"
className="periscope-btn"
icon={<Github size={14} />}
>
{t('view_template')}&nbsp;
<ExternalLink size={14} />
</Button>
</a>
</Flex>
<Button
onClick={handleImport}
loading={isCreating}
className="periscope-btn primary"
type="primary"
>
{t('import_and_next')} &nbsp; <MoveRight size={14} />
</Button>
</div>
</div>
}
>
<div className={styles.contentContainer}>
<div className={styles.contentHeader}>
<Typography.Text>{t('import_json')}</Typography.Text>
</div>
<MEditor
language="json"
height="40vh"
onChange={(newValue): void => setEditorValue(newValue || '')}
value={editorValue}
options={{
scrollbar: { alwaysConsumeMouseWheel: false },
minimap: { enabled: false },
fontSize: 14,
fontFamily: 'Space Mono',
}}
theme={isDarkMode ? 'my-theme' : 'light'}
onMount={(_, monaco): void => {
document.fonts.ready.then(() => {
monaco.editor.remeasureFonts();
});
}}
beforeMount={setEditorTheme}
/>
</div>
</Modal>
);
}
export default ImportJSONModal;

View File

@@ -0,0 +1,154 @@
{
"display": {
"name": "NV dashboard with sections",
"description": ""
},
"datasources": {
"SigNozDatasource": {
"default": true,
"plugin": {
"kind": "signoz/Datasource",
"spec": {}
}
}
},
"panels": {
"b424e23b": {
"kind": "Panel",
"spec": {
"display": {
"name": ""
},
"plugin": {
"kind": "signoz/NumberPanel",
"spec": {
"formatting": {
"unit": "s",
"decimalPrecision": "2"
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "signoz/BuilderQuery",
"spec": {
"name": "A",
"signal": "metrics",
"aggregations": [
{
"metricName": "container.cpu.time",
"reduceTo": "sum",
"spaceAggregation": "sum",
"timeAggregation": "rate"
}
],
"filter": {
"expression": ""
}
}
}
}
}
]
}
},
"251df4d5": {
"kind": "Panel",
"spec": {
"display": {
"name": ""
},
"plugin": {
"kind": "signoz/TimeSeriesPanel",
"spec": {
"visualization": {
"fillSpans": false
},
"formatting": {
"unit": "recommendations",
"decimalPrecision": "2"
},
"chartAppearance": {
"lineInterpolation": "spline",
"showPoints": false,
"lineStyle": "solid",
"fillMode": "none",
"spanGaps": {"fillOnlyBelow": true}
},
"legend": {
"position": "bottom"
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "signoz/BuilderQuery",
"spec": {
"name": "A",
"signal": "metrics",
"aggregations": [
{
"metricName": "app_recommendations_counter",
"reduceTo": "sum",
"spaceAggregation": "sum",
"timeAggregation": "rate"
}
],
"filter": {
"expression": ""
}
}
}
}
}
]
}
}
},
"layouts": [
{
"kind": "Grid",
"spec": {
"display": {
"title": "Bravo"
},
"items": [
{
"x": 0,
"y": 0,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/b424e23b"
}
}
]
}
},
{
"kind": "Grid",
"spec": {
"display": {
"title": "Alpha"
},
"items": [
{
"x": 0,
"y": 0,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/251df4d5"
}
}
]
}
}
]
}

View File

@@ -6,8 +6,9 @@
height: 44px;
flex-shrink: 0;
border-radius: 6px 6px 0px 0px;
border: 1px solid var(--l2-border);
background: var(--l1-background);
border: 1px solid var(--l1-border);
background: var(--l2-background);
box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.1);
}
.label {
@@ -22,36 +23,10 @@
.rightActions {
display: flex;
align-items: center;
gap: 4px;
color: white;
}
.sortPrefix {
color: var(--l3-foreground);
}
// Inline metadata-visibility toggles (replaces the configure modal).
.metaPanel {
display: flex;
flex-direction: column;
min-width: 220px;
padding: 4px;
}
.metaRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 8px 10px;
}
.metaLabel {
color: var(--l2-foreground);
font-size: var(--font-size-sm);
}
// Shared trigger button for the sort + configure-group icons in the right
// actions cluster. Provides a square hover/active background so users know
// which icon they're targeting.

View File

@@ -1,20 +1,17 @@
// eslint-disable-next-line signoz/no-antd-components -- Popover/Tooltip not yet migrated for this menu
import { Popover, Tooltip } from 'antd';
import { Button } from '@signozhq/ui/button';
import { Switch } from '@signozhq/ui/switch';
import { Button, Popover, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { ArrowDown, ArrowUp, Check, HdmiPort } from '@signozhq/icons';
import {
ArrowDownWideNarrow,
Check,
Ellipsis,
HdmiPort,
} from '@signozhq/icons';
import {
DashboardtypesListOrderDTO,
DashboardtypesListSortDTO,
} from 'api/generated/services/sigNoz.schemas';
import {
type DashboardDynamicColumns,
useDashboardsListVisibleColumnsStore,
} from '../../store/useVisibleColumnsStore';
import styles from './ListHeader.module.scss';
interface Props {
@@ -22,178 +19,131 @@ interface Props {
onSortChange: (column: DashboardtypesListSortDTO) => void;
sortOrder: DashboardtypesListOrderDTO;
onOrderChange: (order: DashboardtypesListOrderDTO) => void;
onConfigureMetadata: () => void;
}
const SORT_LABELS: Record<DashboardtypesListSortDTO, string> = {
[DashboardtypesListSortDTO.updated_at]: 'Last updated',
[DashboardtypesListSortDTO.created_at]: 'Last created',
[DashboardtypesListSortDTO.name]: 'Name',
};
// Created-at / created-by are always shown; only the "updated" columns toggle.
const METADATA_COLUMNS: {
key: keyof DashboardDynamicColumns;
label: string;
}[] = [
{ key: 'updatedAt', label: 'Updated at' },
{ key: 'updatedBy', label: 'Updated by' },
];
function ListHeader({
sortColumn,
onSortChange,
sortOrder,
onOrderChange,
onConfigureMetadata,
}: Props): JSX.Element {
const visibleColumns = useDashboardsListVisibleColumnsStore(
(s) => s.visibleColumns,
);
const setVisibleColumns = useDashboardsListVisibleColumnsStore(
(s) => s.setVisibleColumns,
);
const metadataContent = (
<div className={styles.metaPanel}>
<Typography.Text className={styles.sortHeading}>Metadata</Typography.Text>
{METADATA_COLUMNS.map((col) => (
<div key={col.key} className={styles.metaRow}>
<Typography.Text className={styles.metaLabel}>{col.label}</Typography.Text>
<Switch
value={visibleColumns[col.key]}
testId={`metadata-toggle-${col.key}`}
onChange={(checked): void =>
setVisibleColumns({ ...visibleColumns, [col.key]: checked })
}
/>
</div>
))}
</div>
);
return (
<div className={styles.wrapper}>
<Typography.Text className={styles.label}>Results</Typography.Text>
<Typography.Text className={styles.label}>All Dashboards</Typography.Text>
<section className={styles.rightActions}>
<Tooltip title="Sort">
<Popover
trigger="click"
content={
<div className={styles.sortContent}>
<Typography.Text className={styles.sortHeading}>
Sort By
</Typography.Text>
<Button
type="text"
className={styles.sortButton}
onClick={(): void => onSortChange(DashboardtypesListSortDTO.name)}
data-testid="sort-by-name"
>
Name
{sortColumn === DashboardtypesListSortDTO.name && <Check size={14} />}
</Button>
<Button
type="text"
className={styles.sortButton}
onClick={(): void =>
onSortChange(DashboardtypesListSortDTO.created_at)
}
data-testid="sort-by-last-created"
>
Last created
{sortColumn === DashboardtypesListSortDTO.created_at && (
<Check size={14} />
)}
</Button>
<Button
type="text"
className={styles.sortButton}
onClick={(): void =>
onSortChange(DashboardtypesListSortDTO.updated_at)
}
data-testid="sort-by-last-updated"
>
Last updated
{sortColumn === DashboardtypesListSortDTO.updated_at && (
<Check size={14} />
)}
</Button>
<div className={styles.sortDivider} />
<Typography.Text className={styles.sortHeading}>Order</Typography.Text>
<Button
type="text"
className={styles.sortButton}
onClick={(): void => onOrderChange(DashboardtypesListOrderDTO.asc)}
data-testid="sort-order-asc"
>
Ascending
{sortOrder === DashboardtypesListOrderDTO.asc && <Check size={14} />}
</Button>
<Button
type="text"
className={styles.sortButton}
onClick={(): void => onOrderChange(DashboardtypesListOrderDTO.desc)}
data-testid="sort-order-desc"
>
Descending
{sortOrder === DashboardtypesListOrderDTO.desc && <Check size={14} />}
</Button>
</div>
}
rootClassName="sortDashboardsPopover"
placement="bottomRight"
arrow={false}
>
<button
type="button"
className={styles.iconTrigger}
data-testid="sort-by"
aria-label="Sort"
>
<ArrowDownWideNarrow size={14} />
</button>
</Popover>
</Tooltip>
<Popover
trigger="click"
content={
<div className={styles.sortContent}>
<Typography.Text className={styles.sortHeading}>Sort By</Typography.Text>
<Button
variant="ghost"
color="secondary"
className={styles.sortButton}
onClick={(): void => onSortChange(DashboardtypesListSortDTO.name)}
testId="sort-by-name"
suffix={
sortColumn === DashboardtypesListSortDTO.name ? (
<Check size={14} />
) : undefined
}
<div className={styles.configureContent}>
<button
type="button"
className={styles.configureItem}
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
onConfigureMetadata();
}}
data-testid="configure-metadata-trigger"
>
Name
</Button>
<Button
variant="ghost"
color="secondary"
className={styles.sortButton}
onClick={(): void => onSortChange(DashboardtypesListSortDTO.created_at)}
testId="sort-by-last-created"
suffix={
sortColumn === DashboardtypesListSortDTO.created_at ? (
<Check size={14} />
) : undefined
}
>
Last created
</Button>
<Button
variant="ghost"
color="secondary"
className={styles.sortButton}
onClick={(): void => onSortChange(DashboardtypesListSortDTO.updated_at)}
testId="sort-by-last-updated"
suffix={
sortColumn === DashboardtypesListSortDTO.updated_at ? (
<Check size={14} />
) : undefined
}
>
Last updated
</Button>
<div className={styles.sortDivider} />
<Typography.Text className={styles.sortHeading}>Order</Typography.Text>
<Button
variant="ghost"
color="secondary"
className={styles.sortButton}
onClick={(): void => onOrderChange(DashboardtypesListOrderDTO.asc)}
testId="sort-order-asc"
suffix={
sortOrder === DashboardtypesListOrderDTO.asc ? (
<Check size={14} />
) : undefined
}
>
Ascending
</Button>
<Button
variant="ghost"
color="secondary"
className={styles.sortButton}
onClick={(): void => onOrderChange(DashboardtypesListOrderDTO.desc)}
testId="sort-order-desc"
suffix={
sortOrder === DashboardtypesListOrderDTO.desc ? (
<Check size={14} />
) : undefined
}
>
Descending
</Button>
<span className={styles.configureIcon}>
<HdmiPort size={14} />
</span>
<span>Configure metadata</span>
</button>
</div>
}
rootClassName="sortDashboardsPopover"
placement="bottomRight"
arrow={false}
>
<Button
variant="outlined"
color="secondary"
size="sm"
testId="sort-by"
aria-label="Sort"
suffix={
sortOrder === DashboardtypesListOrderDTO.asc ? (
<ArrowUp size={12} />
) : (
<ArrowDown size={12} />
)
}
>
<span className={styles.sortPrefix}>Sort:</span>{' '}
{SORT_LABELS[sortColumn]}{' '}
</Button>
</Popover>
<Popover
trigger="click"
content={metadataContent}
rootClassName="configureGroupPopover"
placement="bottomRight"
arrow={false}
>
<Tooltip title="Metadata">
<Button
variant="ghost"
color="secondary"
size="icon"
aria-label="Metadata"
testId="configure-metadata-trigger"
>
<HdmiPort size={14} />
</Button>
</Tooltip>
<button
type="button"
className={styles.iconTrigger}
aria-label="More options"
>
<Ellipsis size={14} />
</button>
</Popover>
</section>
</div>

View File

@@ -1,148 +0,0 @@
import { type ChangeEvent, useState } from 'react';
// eslint-disable-next-line signoz/no-antd-components -- no @signozhq/ui multiline TextArea yet
import { Input as AntInput } from 'antd';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { Typography } from '@signozhq/ui/typography';
import { toast } from '@signozhq/ui/sonner';
import { AxiosError } from 'axios';
import { generatePath } from 'react-router-dom';
import logEvent from 'api/common/logEvent';
import { createDashboardV2 } from 'api/generated/services/dashboard';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { toPostableTags } from '../../utils';
import styles from './NewDashboardModal.module.scss';
const DEFAULT_NAME = 'Sample Dashboard';
interface Props {
onClose: () => void;
}
function BlankDashboardPanel({ onClose }: Props): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { showErrorModal } = useErrorModal();
const [name, setName] = useState(DEFAULT_NAME);
const [description, setDescription] = useState('');
const [tags, setTags] = useState('');
const [submitting, setSubmitting] = useState(false);
const canSubmit = name.trim().length > 0 && !submitting;
const handleCreate = async (): Promise<void> => {
if (!canSubmit) {
return;
}
try {
setSubmitting(true);
logEvent('Dashboard List: Create dashboard clicked', {});
const postableTags = toPostableTags(tags);
const created = await createDashboardV2({
schemaVersion: 'v6',
generateName: true,
tags: postableTags.length ? postableTags : null,
spec: {
display: {
name: name.trim(),
description: description.trim() || undefined,
},
layouts: [],
panels: {},
variables: [],
},
});
safeNavigate(
generatePath(ROUTES.DASHBOARD, { dashboardId: created.data.id }),
);
} catch (e) {
showErrorModal(e as APIError);
toast.error((e as AxiosError).toString() || 'Failed to create dashboard');
setSubmitting(false);
}
};
return (
<div className={styles.panel}>
<div className={styles.form}>
<div className={styles.field}>
<Typography.Text className={styles.label}>
Title <span className={styles.required}>*</span>
</Typography.Text>
<Input
value={name}
autoFocus
placeholder="e.g. Sample Dashboard"
testId="create-dashboard-name"
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
setName(e.target.value)
}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
void handleCreate();
}
}}
/>
</div>
<div className={styles.field}>
<Typography.Text className={styles.label}>Description</Typography.Text>
{/* eslint-disable-next-line signoz/no-antd-components -- no @signozhq TextArea yet */}
<AntInput.TextArea
value={description}
rows={3}
placeholder="What is this dashboard for?"
data-testid="create-dashboard-description"
onChange={(e): void => setDescription(e.target.value)}
/>
</div>
<div className={styles.field}>
<Typography.Text className={styles.label}>Tags</Typography.Text>
<Input
value={tags}
placeholder="team:jarvis, prod"
testId="create-dashboard-tags"
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
setTags(e.target.value)
}
/>
<Typography.Text className={styles.hint}>
Comma-separated. Use key:value (e.g. team:jarvis) or a single label.
</Typography.Text>
</div>
</div>
<div className={styles.footer}>
<Button
variant="ghost"
color="secondary"
size="md"
onClick={onClose}
testId="create-dashboard-cancel"
>
Cancel
</Button>
<Button
variant="solid"
color="primary"
size="md"
disabled={!canSubmit}
testId="create-dashboard-submit"
onClick={(): void => {
void handleCreate();
}}
>
Create dashboard
</Button>
</div>
</div>
);
}
export default BlankDashboardPanel;

View File

@@ -1,132 +0,0 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { red } from '@ant-design/colors';
// eslint-disable-next-line signoz/no-antd-components -- Upload has no @signozhq/ui equivalent yet
import { Upload, UploadProps } from 'antd';
import { Button } from '@signozhq/ui/button';
import { toast } from '@signozhq/ui/sonner';
import { Typography } from '@signozhq/ui/typography';
import { CircleAlert, MonitorDot, MoveRight } from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import { createDashboardV2 } from 'api/generated/services/dashboard';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { normalizeToPostable } from './importUtils';
import JsonEditor from './JsonEditor';
import styles from './NewDashboardModal.module.scss';
function ImportJsonPanel(): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { t } = useTranslation(['dashboard', 'common']);
const { showErrorModal } = useErrorModal();
const [editorValue, setEditorValue] = useState('');
const [isUploadError, setIsUploadError] = useState(false);
const [isCreateError, setIsCreateError] = useState(false);
const [isCreating, setIsCreating] = useState(false);
const handleUpload: UploadProps['onChange'] = (info) => {
const lastFile = info.fileList[info.fileList.length - 1];
if (!lastFile?.originFileObj) {
return;
}
const reader = new FileReader();
reader.onload = (event): void => {
try {
const target = event.target?.result;
if (!target) {
return;
}
const parsed = JSON.parse(target.toString());
setEditorValue(JSON.stringify(parsed, null, 2));
setIsUploadError(false);
} catch {
setIsUploadError(true);
}
};
reader.readAsText(lastFile.originFileObj);
};
const handleImport = async (): Promise<void> => {
try {
setIsCreating(true);
logEvent('Dashboard List V2: Import and next clicked', {});
const parsed = JSON.parse(editorValue) as Record<string, unknown>;
const payload = normalizeToPostable(parsed);
const response = await createDashboardV2(payload);
safeNavigate(
generatePath(ROUTES.DASHBOARD, { dashboardId: response.data.id }),
);
} catch (error) {
showErrorModal(error as APIError);
setIsCreateError(true);
toast.error(
error instanceof Error ? error.message : t('error_loading_json'),
);
} finally {
setIsCreating(false);
}
};
return (
<div className={styles.panel}>
<Typography.Text className={styles.importHeader}>
{t('import_json')}
</Typography.Text>
<JsonEditor value={editorValue} onChange={setEditorValue} height="280px" />
{(isCreateError || isUploadError) && (
<div className={styles.jsonError}>
<CircleAlert size="md" color={red[7]} />
<Typography className={styles.errorText}>
{isUploadError ? t('error_upload_json') : t('error_loading_json')}
</Typography>
</div>
)}
<div className={styles.importFooter}>
<Upload
accept=".json"
showUploadList={false}
multiple={false}
onChange={handleUpload}
beforeUpload={(): boolean => false}
action="none"
>
<Button
variant="outlined"
color="secondary"
size="md"
prefix={<MonitorDot size={14} />}
testId="upload-json-file"
onClick={(): void => {
logEvent('Dashboard List V2: Upload JSON file clicked', {});
}}
>
{t('upload_json_file')}
</Button>
</Upload>
<Button
variant="solid"
color="primary"
size="md"
loading={isCreating}
suffix={<MoveRight size={14} />}
testId="import-json-submit"
onClick={handleImport}
>
{t('import_and_next')}
</Button>
</div>
</div>
);
}
export default ImportJsonPanel;

View File

@@ -1,90 +0,0 @@
import { useState } from 'react';
import MEditor, { Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { Maximize2 } from '@signozhq/icons';
import { useIsDarkMode } from 'hooks/useDarkMode';
import styles from './NewDashboardModal.module.scss';
interface Props {
value: string;
onChange?: (value: string) => void;
readOnly?: boolean;
height?: string;
}
const defineTheme = (monaco: Monaco): void => {
monaco.editor.defineTheme('my-theme', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'string.key.json', foreground: Color.BG_VANILLA_400 },
{ token: 'string.value.json', foreground: Color.BG_ROBIN_400 },
],
colors: { 'editor.background': Color.BG_INK_300 },
});
};
// JSON editor with a one-click "expand" into an extra-wide modal for easier
// editing/review. The expanded editor shares the same value, so edits persist.
function JsonEditor({
value,
onChange,
readOnly = false,
height = '38vh',
}: Props): JSX.Element {
const isDarkMode = useIsDarkMode();
const [expanded, setExpanded] = useState(false);
const renderEditor = (editorHeight: string): JSX.Element => (
<MEditor
language="json"
height={editorHeight}
value={value}
onChange={(next): void => onChange?.(next || '')}
options={{
readOnly,
scrollbar: { alwaysConsumeMouseWheel: false },
minimap: { enabled: false },
fontSize: 14,
fontFamily: 'Space Mono',
}}
theme={isDarkMode ? 'my-theme' : 'light'}
beforeMount={defineTheme}
/>
);
return (
<div className={styles.editorWrap}>
<Button
variant="ghost"
color="secondary"
size="icon"
className={styles.expandBtn}
aria-label="Expand editor"
testId="json-editor-expand"
onClick={(): void => setExpanded(true)}
>
<Maximize2 size={14} />
</Button>
<div className={styles.editor}>{renderEditor(height)}</div>
<DialogWrapper
title={readOnly ? 'Preview JSON' : 'Edit JSON'}
open={expanded}
width="extra-wide"
onOpenChange={(next): void => {
if (!next) {
setExpanded(false);
}
}}
>
<div className={styles.editorExpanded}>{renderEditor('70vh')}</div>
</DialogWrapper>
</div>
);
}
export default JsonEditor;

View File

@@ -1,195 +0,0 @@
// Fixed height so the modal doesn't resize when switching tabs.
.panel {
display: flex;
flex-direction: column;
gap: 16px;
padding-top: 12px;
height: 460px;
}
.footer {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: auto;
}
.form {
display: flex;
flex-direction: column;
gap: 16px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.label {
font-size: var(--font-size-sm);
color: var(--l2-foreground);
}
.required {
color: var(--danger-background);
}
.hint {
font-size: var(--font-size-xs);
color: var(--l3-foreground);
}
.loading {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: var(--l3-foreground);
font-size: var(--font-size-sm);
}
.spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.cardName {
color: var(--l1-foreground);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
}
.cardDesc {
color: var(--l2-foreground);
font-size: var(--font-size-xs);
line-height: 1.45;
}
.requestRow {
border-top: 1px solid var(--l2-border);
padding-top: 12px;
:global(.request-entity-container) {
gap: 10px 16px;
padding: 14px 16px;
}
}
.importHeader {
font-size: var(--font-size-sm);
color: var(--l2-foreground);
}
.editorWrap {
position: relative;
}
.expandBtn {
position: absolute;
top: 6px;
right: 6px;
z-index: 2;
}
.editor {
border: 1px solid var(--l2-border);
border-radius: 6px;
overflow: hidden;
}
.editorExpanded {
margin-top: 8px;
}
.templatesLayout {
display: flex;
gap: 12px;
flex: 1;
min-height: 0;
}
.templatesList {
flex: none;
width: 200px;
display: flex;
flex-direction: column;
gap: 4px;
overflow-y: auto;
padding-right: 4px;
}
.templateItem {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px 10px;
border: 1px solid transparent;
border-radius: 6px;
background: transparent;
text-align: left;
cursor: pointer;
transition:
background 0.12s,
border-color 0.12s;
}
.templateItem:hover {
background: var(--l2-background);
}
.templateItemActive {
background: var(--l2-background);
border-color: var(--primary-background);
}
.templateName {
color: var(--l1-foreground);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
}
.templateCat {
color: var(--l3-foreground);
font-size: var(--font-size-xs);
}
.templatesPreview {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.previewHead {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.jsonError {
display: flex;
align-items: center;
gap: 8px;
}
.errorText {
color: var(--danger-background);
font-size: var(--font-size-sm);
}
.importFooter {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: auto;
}

View File

@@ -1,59 +0,0 @@
import { useEffect, useState } from 'react';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { Tabs } from '@signozhq/ui/tabs';
import BlankDashboardPanel from './BlankDashboardPanel';
import ImportJsonPanel from './ImportJsonPanel';
import TemplatesPanel from './TemplatesPanel';
interface Props {
open: boolean;
onClose: () => void;
}
function NewDashboardModal({ open, onClose }: Props): JSX.Element {
const [tab, setTab] = useState('blank');
useEffect(() => {
if (open) {
setTab('blank');
}
}, [open]);
return (
<DialogWrapper
title="New dashboard"
open={open}
width="wide"
onOpenChange={(next): void => {
if (!next) {
onClose();
}
}}
>
<Tabs
value={tab}
onChange={(key): void => setTab(key)}
items={[
{
key: 'blank',
label: 'Blank',
children: <BlankDashboardPanel onClose={onClose} />,
},
{
key: 'template',
label: 'From a template',
children: <TemplatesPanel />,
},
{
key: 'import',
label: 'Import JSON',
children: <ImportJsonPanel />,
},
]}
/>
</DialogWrapper>
);
}
export default NewDashboardModal;

View File

@@ -1,139 +0,0 @@
import { useState } from 'react';
import { generatePath } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import { toast } from '@signozhq/ui/sonner';
import { ExternalLink, LoaderCircle } from '@signozhq/icons';
import { AxiosError } from 'axios';
import cx from 'classnames';
import logEvent from 'api/common/logEvent';
import { createDashboardV2 } from 'api/generated/services/dashboard';
import ROUTES from 'constants/routes';
import { RequestDashboardBtn } from 'container/ListOfDashboard/RequestDashboardBtn';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { openInNewTab } from 'utils/navigation';
import { normalizeToPostable } from './importUtils';
import JsonEditor from './JsonEditor';
import { useDashboardTemplates } from './templatesData';
import styles from './NewDashboardModal.module.scss';
// Browse the template gallery (mock data until the API lands): pick one on the
// left to preview its JSON on the right, then use it or open the docs.
function TemplatesPanel(): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { showErrorModal } = useErrorModal();
const { data, isLoading } = useDashboardTemplates(true);
const templates = data ?? [];
const [selectedId, setSelectedId] = useState<string | null>(null);
const [creating, setCreating] = useState(false);
const selected = templates.find((t) => t.id === selectedId) ?? templates[0];
const handleUse = async (): Promise<void> => {
if (!selected) {
return;
}
try {
setCreating(true);
logEvent('Dashboard List: Use template clicked', { template: selected.id });
const parsed = JSON.parse(selected.json) as Record<string, unknown>;
const created = await createDashboardV2(normalizeToPostable(parsed));
safeNavigate(
generatePath(ROUTES.DASHBOARD, { dashboardId: created.data.id }),
);
} catch (e) {
showErrorModal(e as APIError);
toast.error(
(e as AxiosError).toString() || 'Failed to create from template',
);
setCreating(false);
}
};
if (isLoading) {
return (
<div className={styles.panel}>
<div className={styles.loading}>
<LoaderCircle size={18} className={styles.spinner} />
<span>Loading templates</span>
</div>
</div>
);
}
return (
<div className={styles.panel}>
<div className={styles.templatesLayout}>
<div className={styles.templatesList}>
{templates.map((template) => (
<button
key={template.id}
type="button"
className={cx(styles.templateItem, {
[styles.templateItemActive]: selected?.id === template.id,
})}
data-testid={`template-${template.id}`}
onClick={(): void => setSelectedId(template.id)}
>
<span className={styles.templateName}>{template.name}</span>
<span className={styles.templateCat}>{template.category}</span>
</button>
))}
</div>
{selected && (
<div className={styles.templatesPreview}>
<div className={styles.previewHead}>
<div>
<Typography.Text className={styles.cardName}>
{selected.name}
</Typography.Text>
<Typography.Text className={styles.cardDesc}>
{selected.description}
</Typography.Text>
</div>
<Button
variant="ghost"
color="secondary"
size="sm"
suffix={<ExternalLink size={13} />}
onClick={(): void => openInNewTab(selected.href)}
testId="template-docs"
>
Docs
</Button>
</div>
<JsonEditor value={selected.json} readOnly height="240px" />
<div className={styles.footer}>
<Button
variant="solid"
color="primary"
size="md"
loading={creating}
testId="use-template"
onClick={(): void => {
void handleUse();
}}
>
Use template
</Button>
</div>
</div>
)}
</div>
<div className={styles.requestRow}>
<RequestDashboardBtn />
</div>
</div>
);
}
export default TemplatesPanel;

View File

@@ -1,106 +0,0 @@
import { useQuery, type UseQueryResult } from 'react-query';
export interface DashboardTemplate {
id: string;
name: string;
description: string;
category: string;
href: string;
// Importable dashboard definition previewed in the gallery (mock for now).
json: string;
}
// A representative dashboard definition for a template — mock until the API
// returns real ones.
const buildTemplateJson = (
name: string,
description: string,
category: string,
): string =>
JSON.stringify(
{
schemaVersion: 'v6',
generateName: true,
tags: [{ key: 'category', value: category.toLowerCase() }],
spec: {
display: { name, description },
layouts: [],
panels: {},
variables: [],
},
},
null,
2,
);
// Mock catalogue until the templates API lands. Mirrors the public gallery at
// https://signoz.io/docs/dashboards/dashboard-templates/overview/
const BASE_TEMPLATES: Omit<DashboardTemplate, 'json'>[] = [
{
id: 'apm',
name: 'APM Metrics',
description: 'Latency, error rate, and throughput across your services.',
category: 'APM',
href: 'https://signoz.io/docs/dashboards/dashboard-templates/apm/',
},
{
id: 'hostmetrics',
name: 'Host Metrics',
description: 'CPU, memory, disk, and network for your hosts.',
category: 'Infra',
href: 'https://signoz.io/docs/dashboards/dashboard-templates/hostmetrics/',
},
{
id: 'kubernetes',
name: 'Kubernetes Pod Metrics',
description: 'Pod, node, and container health for your clusters.',
category: 'Infra',
href:
'https://signoz.io/docs/dashboards/dashboard-templates/kubernetes-pod-metrics-detailed/',
},
{
id: 'postgres',
name: 'PostgreSQL',
description: 'Connections, throughput, and query performance.',
category: 'Databases',
href: 'https://signoz.io/docs/dashboards/dashboard-templates/postgresql/',
},
{
id: 'redis',
name: 'Redis',
description: 'Memory, commands, and hit-rate for Redis instances.',
category: 'Databases',
href: 'https://signoz.io/docs/dashboards/dashboard-templates/redis/',
},
{
id: 'nginx',
name: 'NGINX',
description: 'Request rate, connections, and error responses.',
category: 'Web servers',
href: 'https://signoz.io/docs/dashboards/dashboard-templates/nginx/',
},
];
const MOCK_TEMPLATES: DashboardTemplate[] = BASE_TEMPLATES.map((t) => ({
...t,
json: buildTemplateJson(t.name, t.description, t.category),
}));
// TODO(@AshwinBhatkal): replace with the real templates API when available.
// The small delay simulates the network round-trip so the loading state is
// exercised (a real API call won't resolve instantly).
const fetchDashboardTemplates = (): Promise<DashboardTemplate[]> =>
new Promise((resolve) => {
setTimeout(() => resolve(MOCK_TEMPLATES), 600);
});
export function useDashboardTemplates(
enabled: boolean,
): UseQueryResult<DashboardTemplate[]> {
return useQuery({
queryKey: ['dashboard-templates'],
queryFn: fetchDashboardTemplates,
enabled,
staleTime: Infinity,
});
}

View File

@@ -9,18 +9,12 @@ interface Props {
value: string;
onChange: (value: string) => void;
onSubmit: () => void;
placeholder?: string;
}
function SearchBar({
value,
onChange,
onSubmit,
placeholder = "Search with DSL (e.g. name CONTAINS 'foo')",
}: Props): JSX.Element {
function SearchBar({ value, onChange, onSubmit }: Props): JSX.Element {
return (
<Input
placeholder={placeholder}
placeholder="Search with DSL (e.g. name CONTAINS 'foo')"
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
suffix={
<button

View File

@@ -1,15 +0,0 @@
.statusBar {
display: flex;
align-items: center;
gap: 14px;
height: 36px;
flex: none;
padding: 0 16px;
border-top: 1px solid var(--l2-border);
background: var(--l1-background);
}
.count {
font-size: var(--font-size-xs);
color: var(--l3-foreground);
}

View File

@@ -1,41 +0,0 @@
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import { PanelLeftClose, PanelLeftOpen } from '@signozhq/icons';
import styles from './StatusBar.module.scss';
interface Props {
collapsed: boolean;
onToggleCollapse: () => void;
count: number;
total: number;
}
function StatusBar({
collapsed,
onToggleCollapse,
count,
total,
}: Props): JSX.Element {
return (
<div className={styles.statusBar}>
<Button
variant="ghost"
color="secondary"
size="sm"
prefix={
collapsed ? <PanelLeftOpen size={14} /> : <PanelLeftClose size={14} />
}
onClick={onToggleCollapse}
testId="dashboards-rail-toggle"
>
{collapsed ? 'Expand' : 'Collapse'}
</Button>
<Typography.Text className={styles.count}>
{count} of {total} dashboards
</Typography.Text>
</div>
);
}
export default StatusBar;

View File

@@ -1,110 +0,0 @@
import { type ChangeEvent, type ReactNode, useEffect, useState } from 'react';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { PopoverSimple } from '@signozhq/ui/popover';
import cx from 'classnames';
import { VIEW_ICON_OPTIONS } from '../../views';
import styles from './ViewsRail.module.scss';
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
onSave: (name: string, icon: string) => void;
trigger: ReactNode;
}
const DEFAULT_ICON = VIEW_ICON_OPTIONS[0].name;
function SaveViewPopover({
open,
onOpenChange,
onSave,
trigger,
}: Props): JSX.Element {
const [name, setName] = useState('');
const [icon, setIcon] = useState(DEFAULT_ICON);
useEffect(() => {
if (open) {
setName('');
setIcon(DEFAULT_ICON);
}
}, [open]);
const canSave = name.trim().length > 0;
const handleSave = (): void => {
if (canSave) {
onSave(name, icon);
onOpenChange(false);
}
};
return (
<PopoverSimple
open={open}
onOpenChange={onOpenChange}
align="start"
trigger={trigger}
>
<div className={styles.savePopover}>
<div className={styles.saveTitle}>Save as view</div>
<span className={styles.saveLabel}>Name</span>
<Input
value={name}
autoFocus
placeholder="e.g. Prod alerts"
testId="save-view-name"
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
setName(e.target.value)
}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
handleSave();
}
}}
/>
<span className={styles.saveLabel}>Icon</span>
<div className={styles.iconGrid}>
{VIEW_ICON_OPTIONS.map(({ name: iconName, Icon }) => (
<button
key={iconName}
type="button"
aria-label={iconName}
className={cx(styles.iconCell, {
[styles.iconCellOn]: icon === iconName,
})}
onClick={(): void => setIcon(iconName)}
>
<Icon size={14} />
</button>
))}
</div>
<div className={styles.saveActions}>
<Button
variant="ghost"
color="secondary"
size="sm"
onClick={(): void => onOpenChange(false)}
>
Cancel
</Button>
<Button
variant="solid"
color="primary"
size="sm"
disabled={!canSave}
testId="save-view-confirm"
onClick={handleSave}
>
Save view
</Button>
</div>
</div>
</PopoverSimple>
);
}
export default SaveViewPopover;

View File

@@ -1,256 +0,0 @@
.rail {
display: flex;
flex-direction: column;
width: 300px;
flex: none;
border-right: 1px solid var(--l2-border);
background: var(--l1-background);
overflow: hidden;
transition: width 0.18s cubic-bezier(0.2, 0.7, 0.3, 1);
}
.collapsed {
width: 0;
border-right-color: transparent;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 14px 12px 10px 16px;
}
.headerTitle {
margin: 0;
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
letter-spacing: -0.01em;
color: var(--l1-foreground);
}
.search {
padding: 0 12px 10px;
}
.searchEmpty {
padding: 12px;
color: var(--l3-foreground);
font-size: var(--font-size-xs);
text-align: center;
}
.scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 0 8px 8px;
}
.groupLabel {
display: flex;
align-items: center;
gap: 6px;
padding: 16px 8px 8px;
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--l3-foreground);
}
.groupLabelSpaced {
margin-top: 10px;
border-top: 1px solid var(--l2-border);
padding-top: 18px;
}
.groupCount {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-normal);
letter-spacing: 0;
color: var(--l3-foreground);
background: var(--l2-background);
border-radius: 10px;
padding: 1px 6px;
}
.row {
display: flex;
align-items: center;
margin: 3px 0;
border-radius: 6px;
transition: background 0.12s;
}
.row:hover {
background: var(--l2-background);
}
.rowActive {
background: var(--l2-background);
}
.item {
// Neutralise the signoz Button defaults so it reads as a full-width,
// left-aligned list row; the row coordinates hover/active colours below.
--button-display: flex;
--button-justify-content: flex-start;
--button-height: auto;
--button-padding: 9px 10px;
--button-gap: 10px;
--button-variant-ghost-background-color: transparent;
--button-variant-ghost-hover-background-color: transparent;
--button-variant-ghost-color: var(--l2-foreground);
--button-variant-ghost-hover-color: var(--l1-foreground);
flex: 1;
min-width: 0;
width: 100%;
font-size: var(--font-size-sm);
}
.row:hover .item,
.rowActive .item {
--button-variant-ghost-color: var(--l1-foreground);
}
.itemIcon {
display: inline-flex;
color: var(--l3-foreground);
}
.rowActive .itemIcon {
color: var(--primary-background);
}
.itemLabel {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dirtyDot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--warning-background);
flex: none;
}
.itemAction {
// Square icon button that surfaces on row hover and turns red on its own
// hover; colours flow through the signoz Button tokens.
--button-height: auto;
--button-padding: 0;
--button-border-radius: 4px;
--button-variant-ghost-background-color: transparent;
--button-variant-ghost-color: var(--l3-foreground);
--button-variant-ghost-hover-background-color: var(--danger-background);
--button-variant-ghost-hover-color: var(--danger-color, #fff);
width: 20px;
height: 20px;
margin-right: 8px;
opacity: 0;
transition: opacity 0.1s;
}
.row:hover .itemAction {
opacity: 1;
}
.empty {
margin: 4px 8px;
padding: 12px;
border: 1px dashed var(--l2-border);
border-radius: 8px;
color: var(--l3-foreground);
font-size: var(--font-size-xs);
line-height: 1.5;
text-align: center;
}
.dirtyPanel {
flex: none;
padding: 12px 14px;
border-top: 1px solid var(--l2-border);
background: var(--l2-background);
}
.dirtyPanelDefault {
background: var(--l1-background);
}
.dirtyTitle {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--l2-foreground);
margin-bottom: 8px;
}
.dirtyActions {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.savePopover {
display: flex;
flex-direction: column;
gap: 8px;
width: 280px;
padding: 4px;
}
.saveTitle {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
color: var(--l1-foreground);
}
.saveLabel {
font-size: var(--font-size-xs);
color: var(--l3-foreground);
margin-top: 2px;
}
.iconGrid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 6px;
}
.iconCell {
display: flex;
align-items: center;
justify-content: center;
height: 34px;
border: 1px solid var(--l2-border);
border-radius: 6px;
background: var(--l2-background);
color: var(--l2-foreground);
cursor: pointer;
transition:
border-color 0.12s,
color 0.12s;
}
.iconCell:hover {
color: var(--l1-foreground);
border-color: var(--l3-foreground);
}
.iconCellOn {
border-color: var(--primary-background);
color: var(--primary-background);
}
.saveActions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 4px;
}

View File

@@ -1,283 +0,0 @@
import { type ChangeEvent, useCallback, useState } from 'react';
import { Modal } from 'antd';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { Typography } from '@signozhq/ui/typography';
import { CircleAlert, Plus, Search, Trash2 } from '@signozhq/icons';
import cx from 'classnames';
import type { SavedView } from '../../types';
import { type BuiltinView, iconByName } from '../../views';
import SaveViewPopover from './SaveViewPopover';
import styles from './ViewsRail.module.scss';
interface Props {
activeViewId: string;
builtinViews: BuiltinView[];
customViews: SavedView[];
isCustomActive: boolean;
isModified: boolean;
collapsed?: boolean;
onSelect: (id: string) => void;
onSave: (name: string, icon: string) => void;
onSaveChanges: () => void;
onReset: () => void;
onClearFilters: () => void;
onDelete: (id: string) => void;
}
interface ViewRow {
id: string;
label: string;
icon: BuiltinView['icon'];
deletable?: boolean;
}
// Purely presentational — active view, dirty state, and handlers come from
// `useActiveView`.
function ViewsRail({
activeViewId,
builtinViews,
customViews,
isCustomActive,
isModified,
collapsed = false,
onSelect,
onSave,
onSaveChanges,
onReset,
onClearFilters,
onDelete,
}: Props): JSX.Element {
const [saveOpen, setSaveOpen] = useState(false);
const [query, setQuery] = useState('');
const [modal, contextHolder] = Modal.useModal();
const q = query.trim().toLowerCase();
const matchesQuery = (label: string): boolean =>
!q || label.toLowerCase().includes(q);
const personal = builtinViews.filter(
(v) => v.section === 'personal' && matchesQuery(v.label),
);
const system = builtinViews.filter(
(v) => v.section === 'system' && matchesQuery(v.label),
);
const custom = customViews.filter((v) => matchesQuery(v.name));
const noMatches =
!!q && personal.length === 0 && system.length === 0 && custom.length === 0;
const confirmDelete = useCallback(
(id: string, label: string): void => {
const { destroy } = modal.confirm({
title: (
<Typography.Title level={5}>
Delete the
<span style={{ color: 'var(--danger-background)', fontWeight: 500 }}>
{' '}
{label}{' '}
</span>
view?
</Typography.Title>
),
content: 'This removes the saved view. Your dashboards are not affected.',
icon: (
<CircleAlert
style={{ color: 'var(--danger-background)', marginInlineEnd: '12px' }}
size="3xl"
/>
),
okText: 'Delete',
okButtonProps: {
danger: true,
onClick: (e): void => {
e.preventDefault();
e.stopPropagation();
onDelete(id);
destroy();
},
},
centered: true,
});
},
[modal, onDelete],
);
const renderItem = (row: ViewRow): JSX.Element => {
const Icon = row.icon;
const active = row.id === activeViewId;
return (
<div key={row.id} className={cx(styles.row, { [styles.rowActive]: active })}>
<Button
variant="ghost"
color="secondary"
className={styles.item}
onClick={(): void => onSelect(row.id)}
testId={`dashboards-view-${row.id}`}
>
<span className={styles.itemIcon}>
<Icon size={14} />
</span>
<span className={styles.itemLabel}>{row.label}</span>
{active && isModified && (
<span className={styles.dirtyDot} title="Unsaved changes" />
)}
</Button>
{row.deletable && (
<Button
variant="ghost"
color="secondary"
size="icon"
className={styles.itemAction}
aria-label="Delete view"
title="Delete view"
onClick={(): void => confirmDelete(row.id, row.label)}
>
<Trash2 size={12} />
</Button>
)}
</div>
);
};
return (
<aside className={cx(styles.rail, { [styles.collapsed]: collapsed })}>
<div className={styles.header}>
<h4 className={styles.headerTitle}>Views</h4>
<SaveViewPopover
open={saveOpen}
onOpenChange={setSaveOpen}
onSave={onSave}
trigger={
<Button
variant="ghost"
color="secondary"
size="icon"
title="Save current filters as a view"
testId="dashboards-view-save-trigger"
>
<Plus size={14} />
</Button>
}
/>
</div>
<div className={styles.search}>
<Input
value={query}
placeholder="Search views"
prefix={<Search size={12} />}
testId="dashboards-view-search"
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
setQuery(e.target.value)
}
/>
</div>
<div className={styles.scroll}>
{personal.length > 0 && (
<>
<div className={styles.groupLabel}>Personal</div>
{personal.map((v) => renderItem(v))}
</>
)}
{system.length > 0 && (
<>
<div className={cx(styles.groupLabel, styles.groupLabelSpaced)}>
System
</div>
{system.map((v) => renderItem(v))}
</>
)}
{(!q || custom.length > 0) && (
<>
<div className={cx(styles.groupLabel, styles.groupLabelSpaced)}>
My views
<span className={styles.groupCount}>{customViews.length}</span>
</div>
{customViews.length === 0 ? (
<div className={styles.empty}>
No saved views yet. Filter the list, then save it as a view.
</div>
) : (
custom.map((v) =>
renderItem({
id: v.id,
label: v.name,
icon: iconByName(v.icon),
deletable: true,
}),
)
)}
</>
)}
{noMatches && (
<div className={styles.searchEmpty}>
No views match &ldquo;{query}&rdquo;
</div>
)}
</div>
{isCustomActive && isModified && (
<div className={styles.dirtyPanel}>
<div className={styles.dirtyTitle}>Unsaved changes</div>
<div className={styles.dirtyActions}>
<Button
variant="solid"
color="primary"
size="sm"
onClick={onSaveChanges}
testId="dashboards-view-save-changes"
>
Save
</Button>
<Button
variant="outlined"
color="secondary"
size="sm"
onClick={(): void => setSaveOpen(true)}
>
Save as
</Button>
<Button variant="ghost" color="secondary" size="sm" onClick={onReset}>
Reset
</Button>
</div>
</div>
)}
{!isCustomActive && isModified && (
<div className={cx(styles.dirtyPanel, styles.dirtyPanelDefault)}>
<div className={styles.dirtyTitle}>Filters active</div>
<div className={styles.dirtyActions}>
<Button
variant="solid"
color="primary"
size="sm"
prefix={<Plus size={12} />}
onClick={(): void => setSaveOpen(true)}
testId="dashboards-view-save-as-new"
>
Save as new view
</Button>
<Button
variant="ghost"
color="secondary"
size="sm"
onClick={onClearFilters}
>
Clear
</Button>
</div>
</div>
)}
{contextHolder}
</aside>
);
}
export default ViewsRail;

View File

@@ -1,18 +1,11 @@
.wrapper {
display: flex;
flex-direction: column;
gap: 12px;
gap: 16px;
margin-top: 16px;
width: 100%;
}
.skeleton {
height: 125px;
width: 100%;
height: 76px;
// antd sizes the inner input element; stretch it to fill the row.
:global(.ant-skeleton-input) {
width: 100% !important;
height: 76px !important;
}
}

View File

@@ -2,14 +2,13 @@ import { Skeleton } from 'antd';
import styles from './LoadingState.module.scss';
const ROWS = [0, 1, 2, 3, 4];
function LoadingState(): JSX.Element {
return (
<div className={styles.wrapper}>
{ROWS.map((row) => (
<Skeleton.Input key={row} active block className={styles.skeleton} />
))}
<Skeleton.Input active size="large" className={styles.skeleton} />
<Skeleton.Input active size="large" className={styles.skeleton} />
<Skeleton.Input active size="large" className={styles.skeleton} />
<Skeleton.Input active size="large" className={styles.skeleton} />
</div>
);
}

View File

@@ -3,15 +3,3 @@
padding: 105px 190px;
gap: 8px;
}
.title {
color: var(--l1-foreground);
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
}
.description {
color: var(--l3-foreground);
font-size: var(--font-size-sm);
text-align: center;
}

View File

@@ -5,20 +5,16 @@ import emptyStateUrl from '@/assets/Icons/emptyState.svg';
import styles from './NoResultsState.module.scss';
interface Props {
title: string;
description?: string;
searchString: string;
}
function NoResultsState({ title, description }: Props): JSX.Element {
function NoResultsState({ searchString }: Props): JSX.Element {
return (
<div className={styles.wrapper}>
<img src={emptyStateUrl} alt="" height={32} width={32} />
<Typography.Text className={styles.title}>{title}</Typography.Text>
{description && (
<Typography.Text className={styles.description}>
{description}
</Typography.Text>
)}
<img src={emptyStateUrl} alt="img" height={32} width={32} />
<Typography.Text>
No dashboards found for {searchString}. Create a new dashboard?
</Typography.Text>
</div>
);
}

View File

@@ -1,85 +0,0 @@
// Pure, side-effect-free helpers that translate the UI filter state into the
// backend list-filter DSL string (`GET /api/v2/dashboards?query=…`). Kept
// testable and free of React so the same builder backs live filtering, saved
// views, and built-in views.
//
// DSL reference (subset used here):
// name CONTAINS 'text'
// created_by = 'a@b.com' | created_by IN ['a@b.com', 'c@d.com']
// updated_at >= '2026-06-01T00:00:00.000Z' (RFC3339)
// locked = true
// clauses joined with AND
import dayjs from 'dayjs';
import type { DashboardFilterState, UpdatedWindow } from './types';
export const DEFAULT_FILTER_STATE: DashboardFilterState = {
search: '',
createdBy: [],
updated: 'any',
};
const UPDATED_WINDOW_DAYS: Record<Exclude<UpdatedWindow, 'any'>, number> = {
today: 1,
'7d': 7,
'30d': 30,
};
// Single-quoted DSL string literal with embedded quotes escaped.
const literal = (value: string): string => `'${value.replace(/'/g, "\\'")}'`;
const updatedClause = (window: UpdatedWindow): string | null => {
if (window === 'any') {
return null;
}
const cutoff = dayjs()
.subtract(UPDATED_WINDOW_DAYS[window], 'day')
.toISOString();
return `updated_at >= ${literal(cutoff)}`;
};
export const filterStateToQuery = (state: DashboardFilterState): string => {
const clauses: string[] = [];
const search = state.search.trim();
if (search) {
clauses.push(`name CONTAINS ${literal(search)}`);
}
if (state.createdBy.length === 1) {
clauses.push(`created_by = ${literal(state.createdBy[0])}`);
} else if (state.createdBy.length > 1) {
clauses.push(`created_by IN [${state.createdBy.map(literal).join(', ')}]`);
}
const updated = updatedClause(state.updated);
if (updated) {
clauses.push(updated);
}
return clauses.join(' AND ');
};
// Combine independent query fragments (e.g. a built-in view's `locked = true`
// plus the live filter state) into a single AND-composed query.
export const combineQueries = (
...parts: (string | null | undefined)[]
): string =>
parts
.map((p) => p?.trim())
.filter((p): p is string => !!p)
.join(' AND ');
export const isFilterStateEmpty = (state: DashboardFilterState): boolean =>
!state.search.trim() &&
state.createdBy.length === 0 &&
state.updated === 'any';
export const areFilterStatesEqual = (
a: DashboardFilterState,
b: DashboardFilterState,
): boolean =>
a.search.trim() === b.search.trim() &&
a.updated === b.updated &&
a.createdBy.length === b.createdBy.length &&
[...a.createdBy].sort().join(',') === [...b.createdBy].sort().join(',');

View File

@@ -1,142 +0,0 @@
import { useCallback, useMemo } from 'react';
import { parseAsString, useQueryState, type Options } from 'nuqs';
import { DEFAULT_FILTER_STATE, areFilterStatesEqual } from '../filterQuery';
import { useDashboardViewsStore } from '../store/useDashboardViewsStore';
import type { DashboardFilterState, SavedView } from '../types';
import {
BUILTIN_VIEWS,
builtinViewQuery,
builtinViewSnapshot,
type BuiltinView,
isClientView,
} from '../views';
const opts: Options = { history: 'push' };
interface UseActiveViewArgs {
filters: DashboardFilterState;
applyFilters: (next: DashboardFilterState) => void;
userEmail: string;
}
export interface UseActiveViewResult {
activeViewId: string;
builtinViews: BuiltinView[];
customViews: SavedView[];
isCustomActive: boolean;
// Current filters diverge from the active view's canonical snapshot.
isModified: boolean;
// Extra server-query fragment the active view contributes, and whether it
// constrains the list client-side (favorites/recent).
viewQuery: string;
clientView: boolean;
selectView: (id: string) => void;
saveView: (name: string, icon: string) => void;
saveActiveView: () => void;
resetView: () => void;
removeView: (id: string) => void;
}
// Orchestrates the active view: which view is selected (URL `view` param),
// merging built-in + persisted custom views, applying a view's snapshot on
// select, dirty detection, and save/reset/delete.
export function useActiveView({
filters,
applyFilters,
userEmail,
}: UseActiveViewArgs): UseActiveViewResult {
const [activeViewId, setActiveViewId] = useQueryState(
'view',
parseAsString.withDefault('all').withOptions(opts),
);
const customViews = useDashboardViewsStore((s) => s.customViews);
const addView = useDashboardViewsStore((s) => s.addView);
const updateView = useDashboardViewsStore((s) => s.updateView);
const deleteView = useDashboardViewsStore((s) => s.deleteView);
const activeCustom = useMemo(
() => customViews.find((v) => v.id === activeViewId),
[customViews, activeViewId],
);
// The filter state the active view "is" — used to detect divergence.
const canonicalSnapshot = useMemo<DashboardFilterState | null>(
() =>
activeCustom
? activeCustom.filters
: builtinViewSnapshot(activeViewId, userEmail),
[activeCustom, activeViewId, userEmail],
);
const isModified = canonicalSnapshot
? !areFilterStatesEqual(filters, canonicalSnapshot)
: false;
const selectView = useCallback(
(id: string): void => {
void setActiveViewId(id);
const custom = customViews.find((v) => v.id === id);
applyFilters(
custom?.filters ??
builtinViewSnapshot(id, userEmail) ??
DEFAULT_FILTER_STATE,
);
},
[setActiveViewId, customViews, applyFilters, userEmail],
);
const saveView = useCallback(
(name: string, icon: string): void => {
const id = `cv_${Date.now()}`;
addView({
id,
name: name.trim(),
icon,
filters: { ...filters },
createdAt: Date.now(),
});
void setActiveViewId(id);
},
[addView, filters, setActiveViewId],
);
const saveActiveView = useCallback((): void => {
if (activeCustom) {
updateView(activeCustom.id, { filters: { ...filters } });
}
}, [activeCustom, updateView, filters]);
const resetView = useCallback((): void => {
if (canonicalSnapshot) {
applyFilters(canonicalSnapshot);
}
}, [canonicalSnapshot, applyFilters]);
const removeView = useCallback(
(id: string): void => {
deleteView(id);
if (activeViewId === id) {
void setActiveViewId('all');
applyFilters(DEFAULT_FILTER_STATE);
}
},
[deleteView, activeViewId, setActiveViewId, applyFilters],
);
return {
activeViewId,
builtinViews: BUILTIN_VIEWS,
customViews,
isCustomActive: !!activeCustom,
isModified,
viewQuery: builtinViewQuery(activeViewId),
clientView: isClientView(activeViewId),
selectView,
saveView,
saveActiveView,
resetView,
removeView,
};
}

View File

@@ -1,102 +0,0 @@
import { useCallback, useMemo } from 'react';
import {
parseAsArrayOf,
parseAsString,
parseAsStringLiteral,
useQueryState,
type Options,
} from 'nuqs';
import {
DEFAULT_FILTER_STATE,
filterStateToQuery,
isFilterStateEmpty,
} from '../filterQuery';
import type { DashboardFilterState, UpdatedWindow } from '../types';
const UPDATED_WINDOWS: UpdatedWindow[] = ['any', 'today', '7d', '30d'];
const opts: Options = { history: 'push' };
export interface UseDashboardFiltersResult {
filters: DashboardFilterState;
// The backend list-filter `query` string derived from the current filters.
query: string;
isEmpty: boolean;
setSearch: (value: string) => void;
setCreatedBy: (emails: string[]) => void;
setUpdated: (window: UpdatedWindow) => void;
// Replace the whole filter state at once — used when applying a saved view.
applyFilters: (next: DashboardFilterState) => void;
clearAll: () => void;
}
// Owns the dashboards-list filter state, synced to the URL (shareable links,
// back/forward) and projected into the backend `query` string. Sort/order/page
// live in their own query-param hooks; this hook is filters-only.
export function useDashboardFilters(): UseDashboardFiltersResult {
const [search, setSearchState] = useQueryState(
'search',
parseAsString.withDefault('').withOptions(opts),
);
const [createdBy, setCreatedByState] = useQueryState(
'createdBy',
parseAsArrayOf(parseAsString).withDefault([]).withOptions(opts),
);
const [updated, setUpdatedState] = useQueryState(
'updated',
parseAsStringLiteral(UPDATED_WINDOWS).withDefault('any').withOptions(opts),
);
const filters = useMemo<DashboardFilterState>(
() => ({ search, createdBy, updated }),
[search, createdBy, updated],
);
const query = useMemo(() => filterStateToQuery(filters), [filters]);
const setSearch = useCallback(
(value: string): void => {
void setSearchState(value);
},
[setSearchState],
);
const setCreatedBy = useCallback(
(emails: string[]): void => {
void setCreatedByState(emails.length ? emails : null);
},
[setCreatedByState],
);
const setUpdated = useCallback(
(window: UpdatedWindow): void => {
void setUpdatedState(window);
},
[setUpdatedState],
);
const applyFilters = useCallback(
(next: DashboardFilterState): void => {
void setSearchState(next.search || null);
void setCreatedByState(next.createdBy.length ? next.createdBy : null);
void setUpdatedState(next.updated);
},
[setSearchState, setCreatedByState, setUpdatedState],
);
const clearAll = useCallback((): void => {
applyFilters(DEFAULT_FILTER_STATE);
}, [applyFilters]);
return {
filters,
query,
isEmpty: isFilterStateEmpty(filters),
setSearch,
setCreatedBy,
setUpdated,
applyFilters,
clearAll,
};
}

View File

@@ -1,75 +0,0 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { LOCALSTORAGE } from 'constants/localStorage';
import type { SavedView } from '../types';
// Most-recently-viewed list is capped so it stays a useful shortlist.
const RECENT_LIMIT = 20;
// Client-side persistence for everything the views feature owns until the views
// API lands: user-saved views, favorite/recently-viewed dashboard ids, and the
// rail collapse preference. Mirrors `useDashboardsListVisibleColumnsStore`.
interface DashboardViewsState {
customViews: SavedView[];
favorites: string[]; // dashboard ids
recent: string[]; // dashboard ids, most-recent first
railCollapsed: boolean;
addView: (view: SavedView) => void;
updateView: (id: string, patch: Partial<Omit<SavedView, 'id'>>) => void;
deleteView: (id: string) => void;
toggleFavorite: (id: string) => void;
markViewed: (id: string) => void;
setRailCollapsed: (collapsed: boolean) => void;
}
const DEFAULT_STATE = {
customViews: [] as SavedView[],
favorites: [] as string[],
recent: [] as string[],
railCollapsed: false,
};
export const useDashboardViewsStore = create<DashboardViewsState>()(
persist(
(set) => ({
...DEFAULT_STATE,
addView: (view): void => {
set((s) => ({ customViews: [...s.customViews, view] }));
},
updateView: (id, patch): void => {
set((s) => ({
customViews: s.customViews.map((v) =>
v.id === id ? { ...v, ...patch } : v,
),
}));
},
deleteView: (id): void => {
set((s) => ({ customViews: s.customViews.filter((v) => v.id !== id) }));
},
toggleFavorite: (id): void => {
set((s) => ({
favorites: s.favorites.includes(id)
? s.favorites.filter((f) => f !== id)
: [...s.favorites, id],
}));
},
markViewed: (id): void => {
set((s) => ({
recent: [id, ...s.recent.filter((r) => r !== id)].slice(0, RECENT_LIMIT),
}));
},
setRailCollapsed: (collapsed): void => {
set({ railCollapsed: collapsed });
},
}),
{
name: LOCALSTORAGE.DASHBOARDS_LIST_VIEWS,
merge: (persisted, current) => ({
...current,
...DEFAULT_STATE,
...((persisted as Partial<DashboardViewsState>) ?? {}),
}),
},
),
);

View File

@@ -1,27 +0,0 @@
// Relative "updated within" windows offered by the Updated filter chip.
export type UpdatedWindow = 'any' | 'today' | '7d' | '30d';
// The user-controllable filter state a view captures. (Tags are intentionally
// excluded for now — the tag filter UI is deferred.) Sort/order are handled
// separately via URL query params and are not part of a view snapshot.
export interface DashboardFilterState {
search: string;
createdBy: string[]; // emails (created_by)
updated: UpdatedWindow;
}
// A saved view: a named, iconed snapshot of filter state. Persisted client-side
// (localStorage) until the views API lands.
export interface SavedView {
id: string;
name: string;
icon: string; // @signozhq/icons icon name
filters: DashboardFilterState;
createdAt: number;
}
// Built-in views rendered above the user's saved views. Their result set is
// derived (a fixed query fragment or a client-side id set), never persisted.
export type BuiltinViewId = 'mine' | 'favorites' | 'recent' | 'all' | 'locked';
export type ViewSection = 'personal' | 'system' | 'custom';

View File

@@ -1,9 +1,6 @@
import dayjs from 'dayjs';
import { isEmpty } from 'lodash-es';
import type {
DashboardtypesListedDashboardV2DTO,
TagtypesPostableTagDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { DashboardtypesListedDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
export type DashboardListItem = DashboardtypesListedDashboardV2DTO;
@@ -14,24 +11,6 @@ export const tagsToStrings = (
tag.key === tag.value ? tag.key : `${tag.key}:${tag.value}`,
);
// Inverse of `tagsToStrings`: each comma-separated tag is "key:value" or a bare
// label (key === value).
export const toPostableTags = (raw: string): TagtypesPostableTagDTO[] =>
raw
.split(',')
.map((label) => label.trim())
.filter(Boolean)
.map((label) => {
const sep = label.indexOf(':');
if (sep > 0) {
return {
key: label.slice(0, sep).trim(),
value: label.slice(sep + 1).trim(),
};
}
return { key: label, value: label };
});
export const lastUpdatedLabel = (time: string | undefined): string => {
if (!time || isEmpty(time)) {
return 'No updates yet!';

View File

@@ -1,164 +0,0 @@
// Built-in view catalogue + the pure logic that maps a view to how it
// constrains the list. Views fall into three mechanisms:
// - snapshot: selecting applies a filter snapshot (All, My dashboards, custom)
// - query: contributes an extra server clause AND-ed with the chips (Locked)
// - client: constrains by a client-side id set (Favorites, Recently viewed)
import {
Activity,
Bookmark,
Clock,
Code,
Flag,
Layers,
Lock,
Server,
Star,
Tag,
User,
} from '@signozhq/icons';
import { DEFAULT_FILTER_STATE } from './filterQuery';
import type { BuiltinViewId, DashboardFilterState, ViewSection } from './types';
import type { DashboardListItem } from './utils';
// All @signozhq icons share this component type.
export type ViewIcon = typeof Star;
export interface BuiltinView {
id: BuiltinViewId;
label: string;
icon: ViewIcon;
section: Exclude<ViewSection, 'custom'>;
}
export const BUILTIN_VIEWS: BuiltinView[] = [
{ id: 'mine', label: 'My dashboards', icon: User, section: 'personal' },
{ id: 'favorites', label: 'Favorites', icon: Star, section: 'personal' },
{ id: 'recent', label: 'Recently viewed', icon: Clock, section: 'personal' },
{ id: 'all', label: 'All dashboards', icon: Layers, section: 'system' },
{ id: 'locked', label: 'Locked', icon: Lock, section: 'system' },
];
// Icons offered when naming a saved view; stored by name on the view.
export const VIEW_ICON_OPTIONS: { name: string; Icon: ViewIcon }[] = [
{ name: 'bookmark', Icon: Bookmark },
{ name: 'star', Icon: Star },
{ name: 'layers', Icon: Layers },
{ name: 'activity', Icon: Activity },
{ name: 'server', Icon: Server },
{ name: 'code', Icon: Code },
{ name: 'flag', Icon: Flag },
{ name: 'tag', Icon: Tag },
{ name: 'lock', Icon: Lock },
{ name: 'clock', Icon: Clock },
];
const ICON_BY_NAME = new Map(VIEW_ICON_OPTIONS.map((o) => [o.name, o.Icon]));
export const iconByName = (name: string): ViewIcon =>
ICON_BY_NAME.get(name) ?? Bookmark;
// Favorites/Recently-viewed constrain by a client-side id set — the backend has
// no id filter, so these are filtered on the fetched rows.
export const isClientView = (id: string): boolean =>
id === 'favorites' || id === 'recent';
// Extra server query fragment a built-in view contributes (AND-ed with chips).
export const builtinViewQuery = (id: string): string =>
id === 'locked' ? 'locked = true' : '';
// The canonical filter snapshot a built-in view applies when selected. `null`
// for ids that aren't built-in (custom views carry their own snapshot).
export const builtinViewSnapshot = (
id: string,
userEmail: string,
): DashboardFilterState | null => {
switch (id) {
case 'mine':
return {
...DEFAULT_FILTER_STATE,
createdBy: userEmail ? [userEmail] : [],
};
case 'all':
case 'favorites':
case 'recent':
case 'locked':
return { ...DEFAULT_FILTER_STATE };
default:
return null;
}
};
export interface EmptyStateCopy {
title: string;
description: string;
}
// Context-aware copy for the no-results state, so an empty Locked view doesn't
// read like a failed search ("No dashboards found for .").
export const noResultsCopy = (
activeViewId: string,
search: string,
hasActiveFilters: boolean,
): EmptyStateCopy => {
const trimmed = search.trim();
if (trimmed) {
return {
title: `No dashboards match "${trimmed}"`,
description: 'Try a different search term or clear your filters.',
};
}
switch (activeViewId) {
case 'favorites':
return {
title: 'No favorite dashboards yet',
description: 'Star a dashboard to pin it here.',
};
case 'recent':
return {
title: 'No recently viewed dashboards',
description: 'Dashboards you open will appear here.',
};
case 'locked':
return {
title: 'No locked dashboards',
description: 'Dashboards locked for editing will appear here.',
};
case 'mine':
return {
title: "You haven't created any dashboards",
description: 'Dashboards you create will appear here.',
};
default:
return hasActiveFilters
? {
title: 'No dashboards match your filters',
description: 'Try adjusting or clearing your filters.',
}
: {
title: 'No dashboards found',
description: 'Create a dashboard to get started.',
};
}
};
// Apply a client-side view's id-set constraint to already-fetched rows.
// Recently-viewed preserves visit order regardless of the active sort.
export const applyClientView = (
items: DashboardListItem[],
id: string,
favorites: string[],
recent: string[],
): DashboardListItem[] => {
if (id === 'favorites') {
const set = new Set(favorites);
return items.filter((d) => set.has(d.id));
}
if (id === 'recent') {
const order = new Map(recent.map((rid, index) => [rid, index]));
return items
.filter((d) => order.has(d.id))
.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
}
return items;
};

View File

@@ -16,7 +16,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListHosts",
Tags: []string{"inframonitoring"},
Summary: "List Hosts for Infra Monitoring",
Description: "Returns a paginated list of hosts with key infrastructure metrics: CPU usage (%), memory usage (%), I/O wait (%), disk usage (%), and 15-minute load average. Each host includes its current status (active/inactive based on metrics reported in the last 10 minutes) and metadata attributes (e.g., os.type). Supports filtering via a filter expression, filtering by host status, custom groupBy to aggregate hosts by any attribute, ordering by any of the five metrics, and pagination via offset/limit. The response type is 'list' for the default host.name grouping or 'grouped_list' for custom groupBy keys. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (cpu, memory, wait, load15, diskUsage) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of hosts with key infrastructure metrics: CPU usage (%), memory usage (%), I/O wait (%), disk usage (%), and 15-minute load average. Each host includes its current status (active/inactive based on metrics reported in the last 10 minutes) and metadata attributes (e.g., os.type). Supports filtering via a filter expression, filtering by host status, custom groupBy to aggregate hosts by any attribute, ordering by any of the five metrics, and pagination via offset/limit. The response type is 'list' for the default host.name grouping or 'grouped_list' for custom groupBy keys. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (cpu, memory, wait, load15, diskUsage) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableHosts),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Hosts),
@@ -35,7 +35,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListPods",
Tags: []string{"inframonitoring"},
Summary: "List Pods for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed/unknown/no_data), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current phase) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase: { pending, running, succeeded, failed, unknown } derived from each pod's latest phase in the window). Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed/unknown/no_data), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current phase) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase: { pending, running, succeeded, failed, unknown } derived from each pod's latest phase in the window). Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostablePods),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Pods),
@@ -54,7 +54,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListNodes",
Tags: []string{"inframonitoring"},
Summary: "List Nodes for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } for pods scheduled on the listed nodes). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } for pods scheduled on the listed nodes). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableNodes),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Nodes),
@@ -73,7 +73,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListNamespaces",
Tags: []string{"inframonitoring"},
Summary: "List Namespaces for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableNamespaces),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Namespaces),
@@ -92,7 +92,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListClusters",
Tags: []string{"inframonitoring"},
Summary: "List Clusters for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableClusters),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Clusters),
@@ -111,7 +111,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListVolumes",
Tags: []string{"inframonitoring"},
Summary: "List Volumes for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes persistent volume claims (PVCs) with key volume metrics: available bytes, capacity bytes, usage (capacity - available), inodes, free inodes, and used inodes. Each row also includes metadata attributes (k8s.persistentvolumeclaim.name, k8s.pod.uid, k8s.pod.name, k8s.namespace.name, k8s.node.name, k8s.statefulset.name, k8s.cluster.name). Supports filtering via a filter expression, custom groupBy to aggregate volumes by any attribute, ordering by any of the six metrics (available, capacity, usage, inodes, inodes_free, inodes_used), and pagination via offset/limit. The response type is 'list' for the default k8s.persistentvolumeclaim.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates volumes in the group. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (volumeAvailable, volumeCapacity, volumeUsage, volumeInodes, volumeInodesFree, volumeInodesUsed) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes persistent volume claims (PVCs) with key volume metrics: available bytes, capacity bytes, usage (capacity - available), inodes, free inodes, and used inodes. Each row also includes metadata attributes (k8s.persistentvolumeclaim.name, k8s.pod.uid, k8s.pod.name, k8s.namespace.name, k8s.node.name, k8s.statefulset.name, k8s.cluster.name). Supports filtering via a filter expression, custom groupBy to aggregate volumes by any attribute, ordering by any of the six metrics (available, capacity, usage, inodes, inodes_free, inodes_used), and pagination via offset/limit. The response type is 'list' for the default k8s.persistentvolumeclaim.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates volumes in the group. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (volumeAvailable, volumeCapacity, volumeUsage, volumeInodes, volumeInodesFree, volumeInodesUsed) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableVolumes),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Volumes),
@@ -130,7 +130,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListDeployments",
Tags: []string{"inframonitoring"},
Summary: "List Deployments for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes Deployments with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest, deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each row also reports the latest known desiredPods (k8s.deployment.desired) and availablePods (k8s.deployment.available) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each deployment includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.deployment.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by deployments in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / available_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods, availablePods) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes Deployments with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest, deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each row also reports the latest known desiredPods (k8s.deployment.desired) and availablePods (k8s.deployment.available) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each deployment includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.deployment.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by deployments in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / available_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods, availablePods) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableDeployments),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Deployments),
@@ -149,7 +149,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListStatefulSets",
Tags: []string{"inframonitoring"},
Summary: "List StatefulSets for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableStatefulSets),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.StatefulSets),
@@ -168,7 +168,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListJobs",
Tags: []string{"inframonitoring"},
Summary: "List Jobs for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value); note podCountsByPhase.failed (current pod-phase) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value); note podCountsByPhase.failed (current pod-phase) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableJobs),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Jobs),
@@ -187,7 +187,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
ID: "ListDaemonSets",
Tags: []string{"inframonitoring"},
Summary: "List DaemonSets for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel when no data is available for that field.",
Description: "Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableDaemonSets),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.DaemonSets),

View File

@@ -73,7 +73,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Description: "This endpoint gets a role",
Request: nil,
RequestContentType: "",
Response: new(authtypes.RoleWithTransactionGroups),
Response: new(authtypes.Role),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
@@ -91,60 +91,6 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.authzHandler.Update, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "UpdateRole",
Tags: []string{"role"},
Summary: "Update role",
Description: "This endpoint updates a role",
Request: new(authtypes.UpdatableRole),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbUpdate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceRole,
Verb: coretypes.VerbUpdate,
Category: coretypes.ActionCategoryAccessControl,
ID: coretypes.PathParam("id"),
Selector: provider.roleSelector,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.authzHandler.Delete, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "DeleteRole",
Tags: []string{"role"},
Summary: "Delete role",
Description: "This endpoint deletes a role",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbDelete)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceRole,
Verb: coretypes.VerbDelete,
Category: coretypes.ActionCategoryAccessControl,
ID: coretypes.PathParam("id"),
Selector: provider.roleSelector,
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(
provider.authzMiddleware.CheckResources(provider.authzHandler.GetObjects, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
@@ -185,7 +131,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: true,
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbUpdate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
@@ -212,7 +158,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: true,
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbUpdate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
@@ -226,5 +172,32 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.authzHandler.Delete, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "DeleteRole",
Tags: []string{"role"},
Summary: "Delete role",
Description: "This endpoint deletes a role",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbDelete)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceRole,
Verb: coretypes.VerbDelete,
Category: coretypes.ActionCategoryAccessControl,
ID: coretypes.PathParam("id"),
Selector: provider.roleSelector,
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -21,7 +21,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: true,
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
@@ -37,7 +37,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Response: nil,
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: true,
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
@@ -54,7 +54,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: true,
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
@@ -88,7 +88,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: true,
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{{Name: authtypes.IdentNProviderTokenizer.StringValue()}},
})).Methods(http.MethodGet).GetError(); err != nil {
return err
@@ -111,23 +111,6 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.CreateUser), handler.OpenAPIDef{
ID: "CreateUser",
Tags: []string{"users"},
Summary: "Create user",
Description: "This endpoint creates a user for the organization",
Request: new(authtypes.PostableUser),
RequestContentType: "application/json",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/users/me", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.UpdateMyUser), handler.OpenAPIDef{
ID: "UpdateMyUserV2",
Tags: []string{"users"},
@@ -156,7 +139,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: true,
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
@@ -190,7 +173,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: true,
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err

View File

@@ -33,8 +33,8 @@ type AuthZ interface {
// Lists the selectors for objects assigned to subject (s) with relation (r) on resource (s)
ListObjects(context.Context, string, authtypes.Relation, coretypes.Type) ([]*coretypes.Object, error)
// Creates the role with its transaction groups.
Create(context.Context, valuer.UUID, *authtypes.RoleWithTransactionGroups) error
// Creates the role.
Create(context.Context, valuer.UUID, *authtypes.Role) error
// Gets the role if it exists or creates one.
GetOrCreate(context.Context, valuer.UUID, *authtypes.Role) (*authtypes.Role, error)
@@ -48,18 +48,12 @@ type AuthZ interface {
// Patches the objects in authorization server associated with the given role and relation
PatchObjects(context.Context, valuer.UUID, string, authtypes.Relation, []*coretypes.Object, []*coretypes.Object) error
// Updates the role's metadata and reconciles its transaction groups.
Update(context.Context, valuer.UUID, *authtypes.RoleWithTransactionGroups) error
// Deletes the role and tuples in authorization server.
Delete(context.Context, valuer.UUID, valuer.UUID) error
// Gets the role
Get(context.Context, valuer.UUID, valuer.UUID) (*authtypes.Role, error)
// Gets the role with transaction groups
GetWithTransactionGroups(context.Context, valuer.UUID, valuer.UUID) (*authtypes.RoleWithTransactionGroups, error)
// Gets the role by org_id and name
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*authtypes.Role, error)
@@ -107,8 +101,6 @@ type Handler interface {
PatchObjects(http.ResponseWriter, *http.Request)
Update(http.ResponseWriter, *http.Request)
Check(http.ResponseWriter, *http.Request)
Delete(http.ResponseWriter, *http.Request)

View File

@@ -83,10 +83,6 @@ func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.
return provider.store.Get(ctx, orgID, id)
}
func (provider *provider) GetWithTransactionGroups(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.RoleWithTransactionGroups, error) {
return nil, errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
return provider.store.GetByOrgIDAndName(ctx, orgID, name)
}
@@ -172,7 +168,7 @@ func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context,
return provider.Grant(ctx, orgID, []string{authtypes.SigNozAdminRoleName}, authtypes.MustNewSubject(coretypes.NewResourceUser(), userID.String(), orgID, nil))
}
func (setter *provider) Create(_ context.Context, _ valuer.UUID, _ *authtypes.RoleWithTransactionGroups) error {
func (setter *provider) Create(_ context.Context, _ valuer.UUID, _ *authtypes.Role) error {
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
}
@@ -184,10 +180,6 @@ func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id
return nil, errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) Update(_ context.Context, _ valuer.UUID, _ *authtypes.RoleWithTransactionGroups) error {
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) Patch(_ context.Context, _ valuer.UUID, _ *authtypes.Role) error {
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
}

View File

@@ -212,30 +212,6 @@ func (server *Server) CheckWithTupleCreationWithoutClaims(ctx context.Context, o
}
func (server *Server) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
maxTuplesPerWrite := server.config.OpenFGA.MaxTuplesPerWrite
if len(additions)+len(deletions) <= maxTuplesPerWrite {
return server.write(ctx, additions, deletions)
}
for idx := 0; idx < len(additions); idx += maxTuplesPerWrite {
end := min(idx+maxTuplesPerWrite, len(additions))
if err := server.write(ctx, additions[idx:end], nil); err != nil {
return err
}
}
for idx := 0; idx < len(deletions); idx += maxTuplesPerWrite {
end := min(idx+maxTuplesPerWrite, len(deletions))
if err := server.write(ctx, nil, deletions[idx:end]); err != nil {
return err
}
}
return nil
}
func (server *Server) write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
if len(additions) == 0 && len(deletions) == 0 {
return nil
}

View File

@@ -36,14 +36,14 @@ func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
return
}
roleWithTransactionGroups := authtypes.NewRoleWithTransactionGroups(req.Name, req.Description, authtypes.RoleTypeCustom, valuer.MustNewUUID(claims.OrgID), req.TransactionGroups)
err = handler.authz.Create(ctx, valuer.MustNewUUID(claims.OrgID), roleWithTransactionGroups)
role := authtypes.NewRole(req.Name, req.Description, authtypes.RoleTypeCustom, valuer.MustNewUUID(claims.OrgID))
err = handler.authz.Create(ctx, valuer.MustNewUUID(claims.OrgID), role)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, types.Identifiable{ID: roleWithTransactionGroups.ID})
render.Success(rw, http.StatusCreated, types.Identifiable{ID: role.ID})
}
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
@@ -65,13 +65,13 @@ func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
return
}
roleWithTransactionGroups, err := handler.authz.GetWithTransactionGroups(ctx, valuer.MustNewUUID(claims.OrgID), roleID)
role, err := handler.authz.Get(ctx, valuer.MustNewUUID(claims.OrgID), roleID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, roleWithTransactionGroups)
render.Success(rw, http.StatusOK, role)
}
func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) {
@@ -224,48 +224,6 @@ func (handler *handler) PatchObjects(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
id, err := valuer.NewUUID(mux.Vars(r)["id"])
if err != nil {
render.Error(rw, err)
return
}
req := new(authtypes.UpdatableRole)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
role, err := handler.authz.Get(ctx, valuer.MustNewUUID(claims.OrgID), id)
if err != nil {
render.Error(rw, err)
return
}
roleWithTransactionGroups := authtypes.MakeRoleWithTransactionGroups(role, nil)
err = roleWithTransactionGroups.Update(req.Description, req.TransactionGroups)
if err != nil {
render.Error(rw, err)
return
}
err = handler.authz.Update(ctx, valuer.MustNewUUID(claims.OrgID), roleWithTransactionGroups)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)

View File

@@ -3,16 +3,15 @@ package flagger
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
var (
FeatureUseSpanMetrics = featuretypes.MustNewName("use_span_metrics")
FeatureKafkaSpanEval = featuretypes.MustNewName("kafka_span_eval")
FeatureHideRootUser = featuretypes.MustNewName("hide_root_user")
FeatureGetMetersFromZeus = featuretypes.MustNewName("get_meters_from_zeus")
FeaturePutMetersInZeus = featuretypes.MustNewName("put_meters_in_zeus")
FeatureUseMeterReporter = featuretypes.MustNewName("use_meter_reporter")
FeatureUseJSONBody = featuretypes.MustNewName("use_json_body")
FeatureUseFineGrainedAuthz = featuretypes.MustNewName("use_fine_grained_authz")
FeatureUseDashboardV2 = featuretypes.MustNewName("use_dashboard_v2")
FeatureEnableAIObservability = featuretypes.MustNewName("enable_ai_observability")
FeatureUseSpanMetrics = featuretypes.MustNewName("use_span_metrics")
FeatureKafkaSpanEval = featuretypes.MustNewName("kafka_span_eval")
FeatureHideRootUser = featuretypes.MustNewName("hide_root_user")
FeatureGetMetersFromZeus = featuretypes.MustNewName("get_meters_from_zeus")
FeaturePutMetersInZeus = featuretypes.MustNewName("put_meters_in_zeus")
FeatureUseMeterReporter = featuretypes.MustNewName("use_meter_reporter")
FeatureUseJSONBody = featuretypes.MustNewName("use_json_body")
FeatureUseFineGrainedAuthz = featuretypes.MustNewName("use_fine_grained_authz")
FeatureUseDashboardV2 = featuretypes.MustNewName("use_dashboard_v2")
)
func MustNewRegistry() featuretypes.Registry {
@@ -89,14 +88,6 @@ func MustNewRegistry() featuretypes.Registry {
DefaultVariant: featuretypes.MustNewName("disabled"),
Variants: featuretypes.NewBooleanVariants(),
},
&featuretypes.Feature{
Name: FeatureEnableAIObservability,
Kind: featuretypes.KindBoolean,
Stage: featuretypes.StageExperimental,
Description: "Controls whether ai observability is enabled",
DefaultVariant: featuretypes.MustNewName("disabled"),
Variants: featuretypes.NewBooleanVariants(),
},
)
if err != nil {
panic(err)

View File

@@ -413,64 +413,27 @@ func (m *module) buildFilterClause(ctx context.Context, filter *qbtypes.Filter,
// NOTE: this method is not specific to infra monitoring — it queries attributes_metadata generically.
// Consider moving to telemetryMetaStore when a second use case emerges.
//
// getMetricsExistenceAndEarliestTime checks which of the given metric names have been
// reported. It returns a list of missing metrics (those not found or with zero count)
// and the earliest first-reported timestamp across all present metrics.
// When all metrics are missing, minFirstReportedUnixMilli is 0.
// TODO(nikhilmantri0902, srikanthccv): This method was designed this way because querier errors if any of the metrics
// in the querier list was never sent, the QueryRange call throws not found error. Modify this method, if QueryRange
// behaviour changes towards this.
func (m *module) getMetricsExistenceAndEarliestTime(ctx context.Context, metricNames []string) ([]string, uint64, error) {
// getEarliestMetricTime returns the earliest first_reported_unix_milli across the
// given metric names. It is used solely for the end-time-before-retention check.
// When none of the metrics have ever been reported, it returns 0.
func (m *module) getEarliestMetricTime(ctx context.Context, metricNames []string) (uint64, error) {
if len(metricNames) == 0 {
return nil, 0, nil
return 0, nil
}
sb := sqlbuilder.NewSelectBuilder()
sb.Select("metric_name", "count(*) AS cnt", "min(first_reported_unix_milli) AS min_first_reported")
sb.Select("min(first_reported_unix_milli) AS min_first_reported")
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.AttributesMetadataTableName))
sb.Where(sb.In("metric_name", sqlbuilder.List(metricNames)))
sb.GroupBy("metric_name")
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
type metricInfo struct {
count uint64
minFirstReported uint64
}
found := make(map[string]metricInfo, len(metricNames))
for rows.Next() {
var name string
var cnt, minFR uint64
if err := rows.Scan(&name, &cnt, &minFR); err != nil {
return nil, 0, err
}
found[name] = metricInfo{count: cnt, minFirstReported: minFR}
}
if err := rows.Err(); err != nil {
return nil, 0, err
var minFirstReported uint64
if err := m.telemetryStore.ClickhouseDB().QueryRow(ctx, query, args...).Scan(&minFirstReported); err != nil {
return 0, err
}
var missingMetrics []string
var globalMinFirstReported uint64
for _, name := range metricNames {
info, ok := found[name]
if !ok || info.count == 0 {
missingMetrics = append(missingMetrics, name)
continue
}
if globalMinFirstReported == 0 || info.minFirstReported < globalMinFirstReported {
globalMinFirstReported = info.minFirstReported
}
}
return missingMetrics, globalMinFirstReported, nil
return minFirstReported, nil
}
// getMetadata fetches the latest values of additionalCols for each unique combination of groupBy keys,

View File

@@ -78,26 +78,18 @@ func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframon
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
}
// 1. Check which required metrics exist and get earliest retention time.
// If any required metric is missing, return early with the list of missing metrics.
// 2. If metrics exist but req.End is before the earliest reported time, return early with endTimeBeforeRetention=true.
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, hostsTableMetricNamesList)
// If req.End is before the earliest reported time for these metrics, return early
// with endTimeBeforeRetention=true.
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, hostsTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.HostRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.HostRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
// TOD(nikhilmantri0902): replace this separate ClickHouse query with a sub-query inside the main query builder query
// once QB supports sub-queries.
@@ -191,23 +183,16 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
}
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, podsTableMetricNamesList)
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, podsTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.PodRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.PodRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getPodsTableMetadata(ctx, req)
if err != nil {
@@ -276,23 +261,16 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
}
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, nodesTableMetricNamesList)
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, nodesTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.NodeRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.NodeRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getNodesTableMetadata(ctx, req)
if err != nil {
@@ -366,23 +344,16 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
}
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, namespacesTableMetricNamesList)
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, namespacesTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.NamespaceRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.NamespaceRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getNamespacesTableMetadata(ctx, req)
if err != nil {
@@ -450,23 +421,16 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
}
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, clustersTableMetricNamesList)
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, clustersTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.ClusterRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.ClusterRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getClustersTableMetadata(ctx, req)
if err != nil {
@@ -547,23 +511,16 @@ func (m *module) ListVolumes(ctx context.Context, orgID valuer.UUID, req *infram
}
req.Filter.Expression = mergeFilterExpressions(volumesBaseFilterExpr, req.Filter.Expression)
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, volumesTableMetricNamesList)
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, volumesTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.VolumeRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.VolumeRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getVolumesTableMetadata(ctx, req)
if err != nil {
@@ -632,23 +589,16 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
}
req.Filter.Expression = mergeFilterExpressions(deploymentsBaseFilterExpr, req.Filter.Expression)
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, deploymentsTableMetricNamesList)
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, deploymentsTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.DeploymentRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.DeploymentRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getDeploymentsTableMetadata(ctx, req)
if err != nil {
@@ -722,23 +672,16 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
}
req.Filter.Expression = mergeFilterExpressions(statefulSetsBaseFilterExpr, req.Filter.Expression)
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, statefulSetsTableMetricNamesList)
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, statefulSetsTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.StatefulSetRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.StatefulSetRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getStatefulSetsTableMetadata(ctx, req)
if err != nil {
@@ -814,23 +757,16 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
}
req.Filter.Expression = mergeFilterExpressions(jobsBaseFilterExpr, req.Filter.Expression)
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, jobsTableMetricNamesList)
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, jobsTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.JobRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.JobRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getJobsTableMetadata(ctx, req)
if err != nil {
@@ -906,23 +842,16 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
}
req.Filter.Expression = mergeFilterExpressions(daemonSetsBaseFilterExpr, req.Filter.Expression)
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, daemonSetsTableMetricNamesList)
minFirstReportedUnixMilli, err := m.getEarliestMetricTime(ctx, daemonSetsTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.DaemonSetRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.DaemonSetRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getDaemonSetsTableMetadata(ctx, req)
if err != nil {

View File

@@ -54,7 +54,7 @@ func (h *handler) List(rw http.ResponseWriter, r *http.Request) {
return
}
rules, total, err := h.module.List(ctx, orgID, q.Offset, q.Limit, q.Search, q.IsOverride)
rules, total, err := h.module.List(ctx, orgID, q.Offset, q.Limit)
if err != nil {
render.Error(rw, err)
return

View File

@@ -21,8 +21,8 @@ func NewModule(store llmpricingruletypes.Store) llmpricingrule.Module {
return &module{store: store}
}
func (module *module) List(ctx context.Context, orgID valuer.UUID, offset, limit int, search string, isOverride *bool) ([]*llmpricingruletypes.LLMPricingRule, int, error) {
return module.store.List(ctx, orgID, offset, limit, search, isOverride)
func (module *module) List(ctx context.Context, orgID valuer.UUID, offset, limit int) ([]*llmpricingruletypes.LLMPricingRule, int, error) {
return module.store.List(ctx, orgID, offset, limit)
}
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*llmpricingruletypes.LLMPricingRule, error) {
@@ -108,7 +108,7 @@ func (module *module) RecommendAgentConfig(orgID valuer.UUID, currentConfYaml []
}
func (module *module) getEnabledRules(ctx context.Context, orgID valuer.UUID) ([]*llmpricingruletypes.LLMPricingRule, error) {
rules, _, err := module.List(ctx, orgID, 0, 10000, "", nil)
rules, _, err := module.List(ctx, orgID, 0, 10000)
if err != nil {
return nil, err
}

View File

@@ -17,25 +17,14 @@ func NewStore(sqlstore sqlstore.SQLStore) llmpricingruletypes.Store {
return &store{sqlstore: sqlstore}
}
func (store *store) List(ctx context.Context, orgID valuer.UUID, offset, limit int, search string, isOverride *bool) ([]*llmpricingruletypes.LLMPricingRule, int, error) {
func (store *store) List(ctx context.Context, orgID valuer.UUID, offset, limit int) ([]*llmpricingruletypes.LLMPricingRule, int, error) {
rules := make([]*llmpricingruletypes.LLMPricingRule, 0)
query := store.sqlstore.
count, err := store.sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&rules).
Where("org_id = ?", orgID)
if search != "" {
like := "%" + search + "%"
query = query.Where("(LOWER(model) LIKE LOWER(?) OR LOWER(provider) LIKE LOWER(?))", like, like)
}
if isOverride != nil {
query = query.Where("is_override = ?", *isOverride)
}
count, err := query.
Where("org_id = ?", orgID).
Order("created_at DESC").
Offset(offset).
Limit(limit).

View File

@@ -13,7 +13,7 @@ type Module interface {
// Since this module interacts with OpAMP, it must implement the AgentFeature interface.
agentConf.AgentFeature
List(ctx context.Context, orgID valuer.UUID, offset, limit int, search string, isOverride *bool) ([]*llmpricingruletypes.LLMPricingRule, int, error)
List(ctx context.Context, orgID valuer.UUID, offset, limit int) ([]*llmpricingruletypes.LLMPricingRule, int, error)
Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*llmpricingruletypes.LLMPricingRule, error)
CreateOrUpdate(ctx context.Context, orgID valuer.UUID, userEmail string, rules []*llmpricingruletypes.UpdatableLLMPricingRule) (err error)
Delete(ctx context.Context, orgID, id valuer.UUID) error

View File

@@ -25,42 +25,6 @@ func NewHandler(setter root.Setter, getter root.Getter) root.Handler {
return &handler{setter: setter, getter: getter}
}
func (handler *handler) CreateUser(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
req := new(authtypes.PostableUser)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
user, err := types.NewUser(req.DisplayName, req.Email, valuer.MustNewUUID(claims.OrgID), types.UserStatusPendingInvite)
if err != nil {
render.Error(rw, err)
return
}
roleIDs := make([]valuer.UUID, 0, len(req.UserRoles))
for _, role := range req.UserRoles {
roleIDs = append(roleIDs, role.ID)
}
user, err = handler.setter.CreatePendingInviteUser(ctx, valuer.MustNewUUID(claims.IdentityID()), valuer.MustNewEmail(claims.Email), req.FrontendBaseUrl, user, root.WithRoleIDs(roleIDs))
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, types.Identifiable{ID: user.ID})
}
func (handler *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()

View File

@@ -215,67 +215,6 @@ func (module *setter) CreateUser(ctx context.Context, user *types.User, opts ...
return nil
}
func (module *setter) CreatePendingInviteUser(ctx context.Context, identityID valuer.UUID, identityEmail valuer.Email, frontendBaseURL string, user *types.User, opts ...root.CreateUserOption) (*types.User, error) {
if err := user.ErrIfNotPending(); err != nil {
return nil, err
}
createUserOpts := root.NewCreateUserOptions(opts...)
roleNames := createUserOpts.RoleNames
if len(createUserOpts.RoleIDs) > 0 {
roles, err := module.authz.ListByOrgIDAndIDs(ctx, user.OrgID, createUserOpts.RoleIDs)
if err != nil {
return nil, err
}
for _, role := range roles {
roleNames = append(roleNames, role.Name)
}
}
var resetPasswordToken *types.ResetPasswordToken
if err := module.store.RunInTx(ctx, func(ctx context.Context) error {
if err := module.createUserWithoutGrant(ctx, user, root.WithRoleNames(roleNames), root.WithFactorPassword(createUserOpts.FactorPassword)); err != nil {
return err
}
token, err := module.GetOrCreateResetPasswordToken(ctx, user.ID)
if err != nil {
return err
}
resetPasswordToken = token
return nil
}); err != nil {
return nil, err
}
module.analytics.TrackUser(ctx, user.OrgID.String(), identityID.String(), "Invite Sent", map[string]any{
"invitee_email": user.Email,
"invitee_role": roleNames,
})
if frontendBaseURL == "" {
module.settings.Logger().InfoContext(ctx, "frontend base url is not provided, skipping email", slog.Any("invitee_email", user.Email))
return user, nil
}
resetLink := resetPasswordToken.FactorPasswordResetLink(frontendBaseURL)
tokenLifetime := module.config.Password.Invite.MaxTokenLifetime
humanizedTokenLifetime := strings.TrimSpace(humanize.RelTime(time.Now(), time.Now().Add(tokenLifetime), "", ""))
if err := module.emailing.SendHTML(ctx, user.Email.String(), "You're Invited to Join SigNoz", emailtypes.TemplateNameInvitationEmail, map[string]any{
"inviter_email": identityEmail.StringValue(),
"link": resetLink,
"Expiry": humanizedTokenLifetime,
}); err != nil {
module.settings.Logger().ErrorContext(ctx, "failed to send invite email", errors.Attr(err))
}
return user, nil
}
func (module *setter) UpdateUserDeprecated(ctx context.Context, orgID valuer.UUID, id string, user *types.DeprecatedUser) (*types.DeprecatedUser, error) {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {

View File

@@ -8,7 +8,6 @@ import (
type createUserOptions struct {
FactorPassword *types.FactorPassword
RoleNames []string
RoleIDs []valuer.UUID
}
type CreateUserOption func(*createUserOptions)
@@ -25,12 +24,6 @@ func WithRoleNames(roleNames []string) CreateUserOption {
}
}
func WithRoleIDs(roleIDs []valuer.UUID) CreateUserOption {
return func(o *createUserOptions) {
o.RoleIDs = roleIDs
}
}
func NewCreateUserOptions(opts ...CreateUserOption) *createUserOptions {
o := &createUserOptions{
FactorPassword: nil,

View File

@@ -45,9 +45,6 @@ type Setter interface {
// invite
CreateBulkInvite(ctx context.Context, orgID valuer.UUID, identityID valuer.UUID, identityEmail valuer.Email, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error)
// Creates a pending invite user with the roles given via opts and emails them the invite link.
CreatePendingInviteUser(ctx context.Context, identityID valuer.UUID, identityEmail valuer.Email, frontendBaseURL string, user *types.User, opts ...CreateUserOption) (*types.User, error)
// Roles
UpdateUserRoles(ctx context.Context, orgID, userID valuer.UUID, finalRoleNames []string) error
AddUserRole(ctx context.Context, orgID, userID valuer.UUID, roleName string) error
@@ -110,7 +107,6 @@ type Handler interface {
// users
ListUsersDeprecated(http.ResponseWriter, *http.Request)
ListUsers(http.ResponseWriter, *http.Request)
CreateUser(http.ResponseWriter, *http.Request)
UpdateUserDeprecated(http.ResponseWriter, *http.Request)
UpdateUser(http.ResponseWriter, *http.Request)
DeleteUser(http.ResponseWriter, *http.Request)

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