mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-14 14:10:32 +01:00
Compare commits
14 Commits
feat/trace
...
ns/scope-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a57b877a7f | ||
|
|
d0370ce3ef | ||
|
|
d169761e65 | ||
|
|
87864ef5d4 | ||
|
|
2e0bc8998e | ||
|
|
7e1f4aa50d | ||
|
|
35da39247c | ||
|
|
ceccc47a34 | ||
|
|
23da5e22ec | ||
|
|
4c1b479149 | ||
|
|
f72204a8b2 | ||
|
|
deb3f385fa | ||
|
|
77ce5f86b1 | ||
|
|
ff211de441 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -231,4 +231,5 @@ cython_debug/
|
||||
# LSP config files
|
||||
pyrightconfig.json
|
||||
|
||||
|
||||
# agents
|
||||
*settings.local.json
|
||||
|
||||
@@ -66,9 +66,10 @@ func runGenerateAuthz(_ context.Context) error {
|
||||
registry := coretypes.NewRegistry()
|
||||
|
||||
allowedResources := map[string]bool{
|
||||
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourceFactorAPIKey).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourcesServiceAccount).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
|
||||
coretypes.NewResourceRef(coretypes.ResourceMetaResourcesRole).String(): true,
|
||||
}
|
||||
|
||||
allowedTypes := map[string]bool{}
|
||||
|
||||
@@ -3,7 +3,7 @@ FROM node:22-bookworm AS build
|
||||
WORKDIR /opt/
|
||||
COPY ./frontend/ ./
|
||||
ENV NODE_OPTIONS=--max-old-space-size=8192
|
||||
RUN CI=1 npm i -g pnpm@10
|
||||
RUN CI=1 npm i -g pnpm
|
||||
RUN CI=1 pnpm install
|
||||
RUN CI=1 pnpm build
|
||||
|
||||
|
||||
@@ -449,7 +449,6 @@ components:
|
||||
- list
|
||||
- assignee
|
||||
- attach
|
||||
- detach
|
||||
type: string
|
||||
AuthtypesRole:
|
||||
properties:
|
||||
@@ -2207,7 +2206,7 @@ components:
|
||||
- role
|
||||
- organization
|
||||
- metaresource
|
||||
- telemetryresource
|
||||
- metaresources
|
||||
type: string
|
||||
DashboardtypesDashboard:
|
||||
properties:
|
||||
@@ -2729,82 +2728,6 @@ components:
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesJobRecord:
|
||||
properties:
|
||||
activePods:
|
||||
type: integer
|
||||
desiredSuccessfulPods:
|
||||
type: integer
|
||||
failedPods:
|
||||
type: integer
|
||||
jobCPU:
|
||||
format: double
|
||||
type: number
|
||||
jobCPULimit:
|
||||
format: double
|
||||
type: number
|
||||
jobCPURequest:
|
||||
format: double
|
||||
type: number
|
||||
jobMemory:
|
||||
format: double
|
||||
type: number
|
||||
jobMemoryLimit:
|
||||
format: double
|
||||
type: number
|
||||
jobMemoryRequest:
|
||||
format: double
|
||||
type: number
|
||||
jobName:
|
||||
type: string
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
successfulPods:
|
||||
type: integer
|
||||
required:
|
||||
- jobName
|
||||
- jobCPU
|
||||
- jobCPURequest
|
||||
- jobCPULimit
|
||||
- jobMemory
|
||||
- jobMemoryRequest
|
||||
- jobMemoryLimit
|
||||
- desiredSuccessfulPods
|
||||
- activePods
|
||||
- failedPods
|
||||
- successfulPods
|
||||
- podCountsByPhase
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesJobs:
|
||||
properties:
|
||||
endTimeBeforeRetention:
|
||||
type: boolean
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesJobRecord'
|
||||
nullable: true
|
||||
type: array
|
||||
requiredMetricsCheck:
|
||||
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
|
||||
total:
|
||||
type: integer
|
||||
type:
|
||||
$ref: '#/components/schemas/InframonitoringtypesResponseType'
|
||||
warning:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryWarnData'
|
||||
required:
|
||||
- type
|
||||
- records
|
||||
- total
|
||||
- requiredMetricsCheck
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesNamespaceRecord:
|
||||
properties:
|
||||
meta:
|
||||
@@ -3108,32 +3031,6 @@ components:
|
||||
- end
|
||||
- limit
|
||||
type: object
|
||||
InframonitoringtypesPostableJobs:
|
||||
properties:
|
||||
end:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
nullable: true
|
||||
type: array
|
||||
limit:
|
||||
type: integer
|
||||
offset:
|
||||
type: integer
|
||||
orderBy:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
|
||||
start:
|
||||
format: int64
|
||||
type: integer
|
||||
required:
|
||||
- start
|
||||
- end
|
||||
- limit
|
||||
type: object
|
||||
InframonitoringtypesPostableNamespaces:
|
||||
properties:
|
||||
end:
|
||||
@@ -9297,9 +9194,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- role:list
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- role:list
|
||||
- ADMIN
|
||||
summary: List roles
|
||||
tags:
|
||||
- role
|
||||
@@ -9371,9 +9268,9 @@ paths:
|
||||
description: Not Implemented
|
||||
security:
|
||||
- api_key:
|
||||
- role:create
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- role:create
|
||||
- ADMIN
|
||||
summary: Create role
|
||||
tags:
|
||||
- role
|
||||
@@ -9433,9 +9330,9 @@ paths:
|
||||
description: Not Implemented
|
||||
security:
|
||||
- api_key:
|
||||
- role:delete
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- role:delete
|
||||
- ADMIN
|
||||
summary: Delete role
|
||||
tags:
|
||||
- role
|
||||
@@ -9484,9 +9381,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- role:read
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- role:read
|
||||
- ADMIN
|
||||
summary: Get role
|
||||
tags:
|
||||
- role
|
||||
@@ -9550,9 +9447,9 @@ paths:
|
||||
description: Not Implemented
|
||||
security:
|
||||
- api_key:
|
||||
- role:update
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- role:update
|
||||
- ADMIN
|
||||
summary: Patch role
|
||||
tags:
|
||||
- role
|
||||
@@ -9628,9 +9525,9 @@ paths:
|
||||
description: Not Implemented
|
||||
security:
|
||||
- api_key:
|
||||
- role:read
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- role:read
|
||||
- ADMIN
|
||||
summary: Get objects for a role by relation
|
||||
tags:
|
||||
- role
|
||||
@@ -9706,9 +9603,9 @@ paths:
|
||||
description: Not Implemented
|
||||
security:
|
||||
- api_key:
|
||||
- role:update
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- role:update
|
||||
- ADMIN
|
||||
summary: Patch objects for a role by relation
|
||||
tags:
|
||||
- role
|
||||
@@ -10312,9 +10209,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- factor-api-key:list
|
||||
- serviceaccount:read
|
||||
- tokenizer:
|
||||
- factor-api-key:list
|
||||
- serviceaccount:read
|
||||
summary: List service account keys
|
||||
tags:
|
||||
- serviceaccount
|
||||
@@ -10380,11 +10277,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- factor-api-key:create
|
||||
- serviceaccount:attach
|
||||
- serviceaccount:update
|
||||
- tokenizer:
|
||||
- factor-api-key:create
|
||||
- serviceaccount:attach
|
||||
- serviceaccount:update
|
||||
summary: Create a service account key
|
||||
tags:
|
||||
- serviceaccount
|
||||
@@ -10437,11 +10332,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- factor-api-key:delete
|
||||
- serviceaccount:detach
|
||||
- serviceaccount:update
|
||||
- tokenizer:
|
||||
- factor-api-key:delete
|
||||
- serviceaccount:detach
|
||||
- serviceaccount:update
|
||||
summary: Revoke a service account key
|
||||
tags:
|
||||
- serviceaccount
|
||||
@@ -10504,9 +10397,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- factor-api-key:update
|
||||
- serviceaccount:update
|
||||
- tokenizer:
|
||||
- factor-api-key:update
|
||||
- serviceaccount:update
|
||||
summary: Updates a service account key
|
||||
tags:
|
||||
- serviceaccount
|
||||
@@ -10678,11 +10571,11 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- serviceaccount:detach
|
||||
- role:detach
|
||||
- serviceaccount:attach
|
||||
- role:attach
|
||||
- tokenizer:
|
||||
- serviceaccount:detach
|
||||
- role:detach
|
||||
- serviceaccount:attach
|
||||
- role:attach
|
||||
summary: Delete service account role
|
||||
tags:
|
||||
- serviceaccount
|
||||
@@ -12418,84 +12311,6 @@ paths:
|
||||
summary: List Hosts for Infra Monitoring
|
||||
tags:
|
||||
- inframonitoring
|
||||
/api/v2/infra_monitoring/jobs:
|
||||
post:
|
||||
deprecated: false
|
||||
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.'
|
||||
operationId: ListJobs
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPostableJobs'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/InframonitoringtypesJobs'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: List Jobs for Infra Monitoring
|
||||
tags:
|
||||
- inframonitoring
|
||||
/api/v2/infra_monitoring/namespaces:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -87,7 +87,7 @@ func (provider *provider) BatchCheck(ctx context.Context, tupleReq map[string]*o
|
||||
}
|
||||
|
||||
func (provider *provider) CheckTransactions(ctx context.Context, subject string, orgID valuer.UUID, transactions []*authtypes.Transaction) ([]*authtypes.TransactionWithAuthorization, error) {
|
||||
tuples, correlations, err := authtypes.NewTuplesFromTransactionsWithCorrelations(transactions, subject, orgID)
|
||||
tuples, err := authtypes.NewTuplesFromTransactions(transactions, subject, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -99,21 +99,10 @@ func (provider *provider) CheckTransactions(ctx context.Context, subject string,
|
||||
|
||||
results := make([]*authtypes.TransactionWithAuthorization, len(transactions))
|
||||
for i, txn := range transactions {
|
||||
txnID := txn.ID.StringValue()
|
||||
authorized := batchResults[txnID].Authorized
|
||||
|
||||
if !authorized {
|
||||
for _, correlationID := range correlations[txnID] {
|
||||
if result, exists := batchResults[correlationID]; exists && result.Authorized {
|
||||
authorized = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := batchResults[txn.ID.StringValue()]
|
||||
results[i] = &authtypes.TransactionWithAuthorization{
|
||||
Transaction: txn,
|
||||
Authorized: authorized,
|
||||
Authorized: result.Authorized,
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
|
||||
@@ -7,27 +7,17 @@ type organization
|
||||
|
||||
type user
|
||||
relations
|
||||
define create: [user, serviceaccount, role#assignee]
|
||||
define list: [user, serviceaccount, role#assignee]
|
||||
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
define update: [user, serviceaccount, role#assignee]
|
||||
define delete: [user, serviceaccount, role#assignee]
|
||||
|
||||
define attach: [user, serviceaccount, role#assignee]
|
||||
define detach: [user, serviceaccount, role#assignee]
|
||||
|
||||
type serviceaccount
|
||||
relations
|
||||
define create: [user, serviceaccount, role#assignee]
|
||||
define list: [user, serviceaccount, role#assignee]
|
||||
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
define update: [user, serviceaccount, role#assignee]
|
||||
define delete: [user, serviceaccount, role#assignee]
|
||||
|
||||
define attach: [user, serviceaccount, role#assignee]
|
||||
define detach: [user, serviceaccount, role#assignee]
|
||||
|
||||
type anonymous
|
||||
|
||||
@@ -35,28 +25,25 @@ type role
|
||||
relations
|
||||
define assignee: [user, serviceaccount, anonymous]
|
||||
|
||||
define create: [user, serviceaccount, role#assignee]
|
||||
define list: [user, serviceaccount, role#assignee]
|
||||
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
define update: [user, serviceaccount, role#assignee]
|
||||
define delete: [user, serviceaccount, role#assignee]
|
||||
|
||||
define attach: [user, serviceaccount, role#assignee]
|
||||
define detach: [user, serviceaccount, role#assignee]
|
||||
|
||||
type metaresource
|
||||
type metaresources
|
||||
relations
|
||||
define create: [user, serviceaccount, role#assignee]
|
||||
define list: [user, serviceaccount, role#assignee]
|
||||
|
||||
define read: [user, serviceaccount, anonymous, role#assignee]
|
||||
define update: [user, serviceaccount, role#assignee]
|
||||
define delete: [user, serviceaccount, role#assignee]
|
||||
type metaresource
|
||||
relations
|
||||
define read: [user, serviceaccount, anonymous, role#assignee]
|
||||
define update: [user, serviceaccount, role#assignee]
|
||||
define delete: [user, serviceaccount, role#assignee]
|
||||
|
||||
define block: [user, serviceaccount, role#assignee]
|
||||
define block: [user, serviceaccount, role#assignee]
|
||||
|
||||
|
||||
type telemetryresource
|
||||
relations
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
@@ -5,15 +5,9 @@ cd frontend && pnpm run commitlint --edit $1
|
||||
|
||||
branch="$(git rev-parse --abbrev-ref HEAD)"
|
||||
|
||||
if [ -n "$TERM" ] && [ "$TERM" != "dumb" ]; then
|
||||
color_red="$(tput setaf 1)"
|
||||
bold="$(tput bold)"
|
||||
reset="$(tput sgr0)"
|
||||
else
|
||||
color_red=""
|
||||
bold=""
|
||||
reset=""
|
||||
fi
|
||||
color_red="$(tput setaf 1)"
|
||||
bold="$(tput bold)"
|
||||
reset="$(tput sgr0)"
|
||||
|
||||
if [ "$branch" = "main" ]; then
|
||||
echo "${color_red}${bold}You can't commit directly to the main branch${reset}"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
registry = 'https://registry.npmjs.org/'
|
||||
engine-strict=true
|
||||
|
||||
public-hoist-pattern[]=@commitlint*
|
||||
public-hoist-pattern[]=commitlint
|
||||
@@ -4,7 +4,6 @@
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"i18n:generate-hash": "node ./i18-generate-hash.cjs",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
@@ -27,8 +26,7 @@
|
||||
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
"pnpm": ">=10.0.0 <11.0.0"
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
@@ -53,7 +51,7 @@
|
||||
"@signozhq/design-tokens": "2.1.4",
|
||||
"@signozhq/icons": "0.4.0",
|
||||
"@signozhq/resizable": "0.0.2",
|
||||
"@signozhq/ui": "0.0.19",
|
||||
"@signozhq/ui": "0.0.18",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
|
||||
76
frontend/pnpm-lock.yaml
generated
76
frontend/pnpm-lock.yaml
generated
@@ -89,8 +89,8 @@ importers:
|
||||
specifier: 0.0.2
|
||||
version: 0.0.2(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@signozhq/ui':
|
||||
specifier: 0.0.19
|
||||
version: 0.0.19(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.27.0(react@18.2.0))(react@18.2.0)
|
||||
specifier: 0.0.18
|
||||
version: 0.0.18(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.27.0(react@18.2.0))(react@18.2.0)
|
||||
'@tanstack/react-table':
|
||||
specifier: 8.21.3
|
||||
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -1907,105 +1907,89 @@ packages:
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
@@ -2360,56 +2344,48 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-arm64-musl@0.47.0':
|
||||
resolution: {integrity: sha512-IxtQC/sbBi4ubbY+MdwdanRWrG9InQJVZqyMsBa5IUaQcnSg86gQme574HxXMC1p4bo4YhV99zQ+wNnGCvEgzw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.47.0':
|
||||
resolution: {integrity: sha512-EWXEhOMbWO0q6eJSbu0QLkU8cKi0ljlYLngeDs2Ocu/pm1rrLwyQiYzlFbdnMRURI4w9ndr1sI9rSbhlJ5o23Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.47.0':
|
||||
resolution: {integrity: sha512-tZrjS11TUiDuEpRaqdk8K9F9xETRyKXfuZKmdeW+Gj7coBnm7+8sBEfyt033EAFEQSlkniAXvBLh+Qja2ioGBQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.47.0':
|
||||
resolution: {integrity: sha512-KBFy+2CFKUCZzYwX2ZOPQKck1vjQbz+hextuc19G4r0WRJwadfAeuQMQRQvB+Ivc8brlbOVg7et8K7E467440g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.47.0':
|
||||
resolution: {integrity: sha512-REUPFKVGSiK99B+9eaPhluEVglzaoj/SMykNC5SUiV2RSsBfV5lWN7Y0iCIc251Wz3GaeAGZsJ/zj3gjarxdFg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-gnu@0.47.0':
|
||||
resolution: {integrity: sha512-KVftVSVEDeIfRW3TIeLe3aNI/iY4m1fu5mDwHcisKMZSCMKLkrhFsjowC7o9RoqNPxbbglm2+/6KAKBIts2t0Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-musl@0.47.0':
|
||||
resolution: {integrity: sha512-DTsmGEaA2860Aq5VUyDO8/MT9NFxwVL93RnRYmpMwK6DsSkThmvEpqoUDDljziEpAedMRG19SCogrNbINSbLUQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-openharmony-arm64@0.47.0':
|
||||
resolution: {integrity: sha512-8r5BDro7fLOBoq1JXHLVSs55OlrxQhEso4HVo0TcY7OXJUPYfjPoOaYL5us+yIwqyP9rQwN+rxuiNFSmaxSuOQ==}
|
||||
@@ -2512,56 +2488,48 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-arm64-musl@1.62.0':
|
||||
resolution: {integrity: sha512-8eCy3FCDuWUM5hWujAv6heMvfZPbcCOU3SdQUAkixZLu5bSzOkNfirJiLGoQFO943xceOKkiQRMQNzH++jM3WA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.62.0':
|
||||
resolution: {integrity: sha512-NjQ7K7tpTPDe9J+yq8p/s/J0E7lRCkK2uDBDqvT4XIT6f4Z0tlnr59OBg/WcrmVHER1AbrcfyxhGTXgcG8ytWg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.62.0':
|
||||
resolution: {integrity: sha512-oKZed9gmSwze29dEt3/Wnsv6l/Ygw/FUst+8Kfpv2SGeS/glEoTGZAMQw37SVyzFV76UTHJN2snGgxK2t2+8ow==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-musl@1.62.0':
|
||||
resolution: {integrity: sha512-gBjBxQ+9lGpAYq+ELqw0w8QXsBnkZclFc7GRX2r0LnEVn3ZTEqeIKpKcGjucmp76Q53bvJD0i4qBWBhcfhSfGA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-s390x-gnu@1.62.0':
|
||||
resolution: {integrity: sha512-Ew2Kxs9EQ9/mbAIJ2hvocMC0wsOu6YKzStI2eFBDt+Td5O8seVC/oxgRIHqCcl5sf5ratA1nozQBAuv7tphkHg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-gnu@1.62.0':
|
||||
resolution: {integrity: sha512-5z25jcAA0gfKyVwz71A0VXgaPlocPoTAxhlv/hgoK6tlCrfoNuw7haWbDHvGMfjXhdic4EqVXGRv5XsTqFnbRQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-musl@1.62.0':
|
||||
resolution: {integrity: sha512-IWpHmMB6ZDllPvqWDkG6AmXrN7JF5e/c4g/0PuURsmlK+vHoYZPB70rr4u1bn3I4LsKCSpqqfveyx6UCOC8wdg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-openharmony-arm64@1.62.0':
|
||||
resolution: {integrity: sha512-fjlSxxrD5pA594vkyikCS9MnPRjQawW6/BLgyTYkO+73wwPlYjkcZ7LSd974l0Q2zkHQmu4DPvJFLYA7o8xrxQ==}
|
||||
@@ -2616,42 +2584,36 @@ packages:
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
|
||||
@@ -3512,28 +3474,24 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==}
|
||||
@@ -3686,8 +3644,8 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18.2.0
|
||||
|
||||
'@signozhq/ui@0.0.19':
|
||||
resolution: {integrity: sha512-2q6aRxN/PR4PlR2xJZAREEuvLPiDFggfFKzCW2Z5vHVVbrgnvZHWD1jPUuwszfEg0ceH3UvkwqceO7wN4uRJAA==}
|
||||
'@signozhq/ui@0.0.18':
|
||||
resolution: {integrity: sha512-1p3ALh76kafiz5yX7ReNKVcHDt2od7CcZD/Vx9i2adTwTeynkLJcEfVoXoJD3oh1kKTleooOiOjRyxlA7VzmSA==}
|
||||
peerDependencies:
|
||||
'@signozhq/icons': 0.3.0
|
||||
react: ^18.2.0
|
||||
@@ -4308,49 +4266,41 @@ packages:
|
||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||
@@ -4417,7 +4367,7 @@ packages:
|
||||
resolution: {integrity: sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
vite: npm:rolldown-vite@7.3.1
|
||||
|
||||
'@webassemblyjs/ast@1.14.1':
|
||||
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
|
||||
@@ -7244,28 +7194,24 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.31.1:
|
||||
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.31.1:
|
||||
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}
|
||||
@@ -10293,7 +10239,7 @@ packages:
|
||||
oxlint: '>=1'
|
||||
stylelint: '>=16'
|
||||
typescript: '*'
|
||||
vite: '>=5.4.21'
|
||||
vite: npm:rolldown-vite@7.3.1
|
||||
vls: '*'
|
||||
vti: '*'
|
||||
vue-tsc: ~2.2.10 || ^3.0.0
|
||||
@@ -10322,12 +10268,12 @@ packages:
|
||||
vite-plugin-compression@0.5.1:
|
||||
resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==}
|
||||
peerDependencies:
|
||||
vite: '>=2.0.0'
|
||||
vite: npm:rolldown-vite@7.3.1
|
||||
|
||||
vite-plugin-html@3.2.2:
|
||||
resolution: {integrity: sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q==}
|
||||
peerDependencies:
|
||||
vite: '>=2.0.0'
|
||||
vite: npm:rolldown-vite@7.3.1
|
||||
|
||||
vite-plugin-image-optimizer@2.0.3:
|
||||
resolution: {integrity: sha512-1vrFOTcpSvv6DCY7h8UXab4wqMAjTJB/ndOzG/Kmj1oDOuPF6mbjkNQoGzzCEYeWGe7qU93jc8oQqvoJ57al3A==}
|
||||
@@ -10335,7 +10281,7 @@ packages:
|
||||
peerDependencies:
|
||||
sharp: '>=0.34.0'
|
||||
svgo: '>=4'
|
||||
vite: '>=5'
|
||||
vite: npm:rolldown-vite@7.3.1
|
||||
peerDependenciesMeta:
|
||||
sharp:
|
||||
optional: true
|
||||
@@ -10345,7 +10291,7 @@ packages:
|
||||
vite-tsconfig-paths@6.1.1:
|
||||
resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==}
|
||||
peerDependencies:
|
||||
vite: '*'
|
||||
vite: npm:rolldown-vite@7.3.1
|
||||
|
||||
void-elements@3.1.0:
|
||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||
@@ -13955,7 +13901,7 @@ snapshots:
|
||||
- react-dom
|
||||
- tailwindcss
|
||||
|
||||
'@signozhq/ui@0.0.19(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.27.0(react@18.2.0))(react@18.2.0)':
|
||||
'@signozhq/ui@0.0.18(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.27.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@chenglou/pretext': 0.0.5
|
||||
'@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
|
||||
@@ -26,6 +26,5 @@
|
||||
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
|
||||
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
|
||||
"your_graph_build_with": "Your graph built with",
|
||||
"dashboard_ok_confirm": "query will be saved. Press OK to confirm.",
|
||||
"variable_name_already_exists": "Variable \"{{name}}\" already exists"
|
||||
"dashboard_ok_confirm": "query will be saved. Press OK to confirm."
|
||||
}
|
||||
|
||||
@@ -30,6 +30,5 @@
|
||||
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
|
||||
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
|
||||
"your_graph_build_with": "Your graph built with",
|
||||
"dashboard_ok_confirm": "query will be saved. Press OK to confirm.",
|
||||
"variable_name_already_exists": "Variable \"{{name}}\" already exists"
|
||||
"dashboard_ok_confirm": "query will be saved. Press OK to confirm."
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import type {
|
||||
InframonitoringtypesPostableClustersDTO,
|
||||
InframonitoringtypesPostableDeploymentsDTO,
|
||||
InframonitoringtypesPostableHostsDTO,
|
||||
InframonitoringtypesPostableJobsDTO,
|
||||
InframonitoringtypesPostableNamespacesDTO,
|
||||
InframonitoringtypesPostableNodesDTO,
|
||||
InframonitoringtypesPostablePodsDTO,
|
||||
@@ -24,7 +23,6 @@ import type {
|
||||
ListClusters200,
|
||||
ListDeployments200,
|
||||
ListHosts200,
|
||||
ListJobs200,
|
||||
ListNamespaces200,
|
||||
ListNodes200,
|
||||
ListPods200,
|
||||
@@ -288,90 +286,6 @@ export const useListHosts = <
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* 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.
|
||||
* @summary List Jobs for Infra Monitoring
|
||||
*/
|
||||
export const listJobs = (
|
||||
inframonitoringtypesPostableJobsDTO: BodyType<InframonitoringtypesPostableJobsDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListJobs200>({
|
||||
url: `/api/v2/infra_monitoring/jobs`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: inframonitoringtypesPostableJobsDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListJobsMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listJobs>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableJobsDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listJobs>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableJobsDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['listJobs'];
|
||||
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 listJobs>>,
|
||||
{ data: BodyType<InframonitoringtypesPostableJobsDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return listJobs(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type ListJobsMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listJobs>>
|
||||
>;
|
||||
export type ListJobsMutationBody =
|
||||
BodyType<InframonitoringtypesPostableJobsDTO>;
|
||||
export type ListJobsMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List Jobs for Infra Monitoring
|
||||
*/
|
||||
export const useListJobs = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof listJobs>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableJobsDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof listJobs>>,
|
||||
TError,
|
||||
{ data: BodyType<InframonitoringtypesPostableJobsDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getListJobsMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* 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.
|
||||
* @summary List Namespaces for Infra Monitoring
|
||||
|
||||
@@ -1840,7 +1840,6 @@ export enum AuthtypesRelationDTO {
|
||||
list = 'list',
|
||||
assignee = 'assignee',
|
||||
attach = 'attach',
|
||||
detach = 'detach',
|
||||
}
|
||||
export interface AuthtypesRoleDTO {
|
||||
/**
|
||||
@@ -4162,7 +4161,7 @@ export enum CoretypesTypeDTO {
|
||||
role = 'role',
|
||||
organization = 'organization',
|
||||
metaresource = 'metaresource',
|
||||
telemetryresource = 'telemetryresource',
|
||||
metaresources = 'metaresources',
|
||||
}
|
||||
export interface DashboardtypesDashboardDTO {
|
||||
/**
|
||||
@@ -4791,91 +4790,6 @@ export interface InframonitoringtypesHostsDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesJobRecordDTOMeta = {
|
||||
[key: string]: string;
|
||||
} | null;
|
||||
|
||||
export interface InframonitoringtypesJobRecordDTO {
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
activePods: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
desiredSuccessfulPods: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
failedPods: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
jobCPU: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
jobCPULimit: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
jobCPURequest: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
jobMemory: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
jobMemoryLimit: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
jobMemoryRequest: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
jobName: string;
|
||||
/**
|
||||
* @type object
|
||||
* @nullable true
|
||||
*/
|
||||
meta: InframonitoringtypesJobRecordDTOMeta;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
successfulPods: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesJobsDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
records: InframonitoringtypesJobRecordDTO[] | null;
|
||||
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
total: number;
|
||||
type: InframonitoringtypesResponseTypeDTO;
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
@@ -5192,34 +5106,6 @@ export interface InframonitoringtypesPostableHostsDTO {
|
||||
start: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPostableJobsDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
groupBy?: Querybuildertypesv5GroupByKeyDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
limit: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
offset?: number;
|
||||
orderBy?: Querybuildertypesv5OrderByDTO;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
start: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPostableNamespacesDTO {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -9849,14 +9735,6 @@ export type ListHosts200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListJobs200 = {
|
||||
data: InframonitoringtypesJobsDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListNamespaces200 = {
|
||||
data: InframonitoringtypesNamespacesDTO;
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Dot, Sparkles } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { Popover } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
@@ -97,7 +97,7 @@ function HeaderRightSection({
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<TooltipSimple title="AI Assistant">
|
||||
<Tooltip title="AI Assistant">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
@@ -113,7 +113,7 @@ function HeaderRightSection({
|
||||
>
|
||||
AI Assistant
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1155,7 +1155,7 @@ describe('removeKeysFromExpression', () => {
|
||||
});
|
||||
|
||||
describe('Real-world scenarios', () => {
|
||||
it('should remove at most one variable expression per key', () => {
|
||||
it('should handle multiple variable instances of same key', () => {
|
||||
const expression =
|
||||
"deployment.environment = $env1 deployment.environment = $env2 deployment.environment = 'default'";
|
||||
const result = removeKeysFromExpression(
|
||||
@@ -1164,11 +1164,9 @@ describe('removeKeysFromExpression', () => {
|
||||
true,
|
||||
);
|
||||
|
||||
// Should remove one occurrence — having multiple $-value clauses for the
|
||||
// same key is invalid. The first is removed; subsequent $ clauses and
|
||||
// literal-value clauses are preserved.
|
||||
// Should remove one occurence as this case in itself is invalid to have multiple variable expressions for the same key
|
||||
expect(result).toBe(
|
||||
"deployment.environment = $env2 AND deployment.environment = 'default'",
|
||||
"deployment.environment = $env1 deployment.environment = 'default'",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1201,186 +1199,6 @@ describe('removeKeysFromExpression', () => {
|
||||
expect(pairs).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ANTLR-based removal — operator precedence (AND binds tighter than OR)', () => {
|
||||
it('preserves OR when removing from a mixed AND/OR expression', () => {
|
||||
// a AND b OR c — grammar parses as (a AND b) OR c
|
||||
// removing b collapses the AND group to just a, OR is preserved
|
||||
expect(
|
||||
removeKeysFromExpression("a = '1' AND b = '2' OR c = '3'", ['b']),
|
||||
).toBe("a = '1' OR c = '3'");
|
||||
});
|
||||
|
||||
it('preserves correct conjunctions in a four-term mixed expression', () => {
|
||||
// a AND b OR c AND d — removing b collapses first AND group to a
|
||||
expect(
|
||||
removeKeysFromExpression("a = '1' AND b = '2' OR c = '3' AND d = '4'", [
|
||||
'b',
|
||||
]),
|
||||
).toBe("a = '1' OR c = '3' AND d = '4'");
|
||||
});
|
||||
|
||||
it('preserves correct conjunctions when removing from a trailing AND group', () => {
|
||||
// a OR b AND c OR d — removing c collapses second AND group to b
|
||||
expect(
|
||||
removeKeysFromExpression("a = '1' OR b = '2' AND c = '3' OR d = '4'", [
|
||||
'c',
|
||||
]),
|
||||
).toBe("a = '1' OR b = '2' OR d = '4'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('ANTLR-based removal — parenthesised expressions', () => {
|
||||
it('removes last clause without leaving a dangling AND', () => {
|
||||
const expression =
|
||||
'(deployment.environment = $deployment.environment AND service.name = $service.name AND top_level_operation IN [$top_level_operation])';
|
||||
expect(
|
||||
removeKeysFromExpression(expression, ['top_level_operation'], true),
|
||||
).toBe(
|
||||
'(deployment.environment = $deployment.environment AND service.name = $service.name)',
|
||||
);
|
||||
});
|
||||
|
||||
it('removes first clause without leaving a dangling AND', () => {
|
||||
expect(
|
||||
removeKeysFromExpression(
|
||||
'(deployment.environment = $deployment.environment AND service.name = $service.name)',
|
||||
['deployment.environment'],
|
||||
true,
|
||||
),
|
||||
).toBe('service.name = $service.name');
|
||||
});
|
||||
|
||||
it('removes middle clause without disturbing surrounding AND', () => {
|
||||
expect(
|
||||
removeKeysFromExpression(
|
||||
'(deployment.environment = $deployment.environment AND service.name = $service.name AND region = $region)',
|
||||
['service.name'],
|
||||
true,
|
||||
),
|
||||
).toBe(
|
||||
'(deployment.environment = $deployment.environment AND region = $region)',
|
||||
);
|
||||
});
|
||||
|
||||
it('drops the empty paren group when its only child is removed', () => {
|
||||
// (a) OR (b) — removing a must not leave () OR (b = '2')
|
||||
// The remaining single-clause group has its redundant parens stripped too
|
||||
expect(removeKeysFromExpression("(a = '1') OR (b = '2')", ['a'])).toBe(
|
||||
"b = '2'",
|
||||
);
|
||||
});
|
||||
|
||||
it('handles OR inside parentheses without leaving dangling OR', () => {
|
||||
expect(
|
||||
removeKeysFromExpression(
|
||||
'(service.name = $service.name OR operation = $operation)',
|
||||
['operation'],
|
||||
true,
|
||||
),
|
||||
).toBe('service.name = $service.name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ANTLR-based removal — BETWEEN, EXISTS, and other operators', () => {
|
||||
it('removes a BETWEEN clause without treating its AND as a conjunction', () => {
|
||||
// BETWEEN x AND y — the AND is part of the operator, not a conjunction
|
||||
expect(
|
||||
removeKeysFromExpression("a BETWEEN 1 AND 10 AND b = '2'", ['a']),
|
||||
).toBe("b = '2'");
|
||||
});
|
||||
|
||||
it('removes a NOT BETWEEN clause without treating its AND as a conjunction', () => {
|
||||
expect(
|
||||
removeKeysFromExpression("a NOT BETWEEN 1 AND 10 AND b = '2'", ['a']),
|
||||
).toBe("b = '2'");
|
||||
});
|
||||
|
||||
it('removes an EXISTS clause (no value token)', () => {
|
||||
expect(removeKeysFromExpression("a = '1' AND b EXISTS", ['b'])).toBe(
|
||||
"a = '1'",
|
||||
);
|
||||
});
|
||||
|
||||
it('removes a NOT EXISTS clause', () => {
|
||||
expect(removeKeysFromExpression("a = '1' AND b NOT EXISTS", ['b'])).toBe(
|
||||
"a = '1'",
|
||||
);
|
||||
});
|
||||
|
||||
it('removes an IN clause correctly', () => {
|
||||
expect(
|
||||
removeKeysFromExpression("service IN ['api', 'web'] AND status = 'ok'", [
|
||||
'service',
|
||||
]),
|
||||
).toBe("status = 'ok'");
|
||||
});
|
||||
|
||||
it('removes a NOT IN clause correctly', () => {
|
||||
expect(
|
||||
removeKeysFromExpression(
|
||||
"service NOT IN ['api', 'web'] AND status = 'ok'",
|
||||
['service'],
|
||||
),
|
||||
).toBe("status = 'ok'");
|
||||
});
|
||||
|
||||
it('removes a CONTAINS clause correctly', () => {
|
||||
expect(
|
||||
removeKeysFromExpression("message CONTAINS 'error' AND service = 'api'", [
|
||||
'message',
|
||||
]),
|
||||
).toBe("service = 'api'");
|
||||
});
|
||||
|
||||
it('removes a LIKE clause correctly', () => {
|
||||
expect(
|
||||
removeKeysFromExpression("message LIKE '%error%' AND service = 'api'", [
|
||||
'message',
|
||||
]),
|
||||
).toBe("service = 'api'");
|
||||
});
|
||||
|
||||
it('removes a NOT LIKE clause correctly', () => {
|
||||
expect(
|
||||
removeKeysFromExpression("message NOT LIKE '%error%' AND service = 'api'", [
|
||||
'message',
|
||||
]),
|
||||
).toBe("service = 'api'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('ANTLR-based removal — AND/OR precedence combinations', () => {
|
||||
it('handles a AND b AND c OR d: removing b leaves a AND c OR d', () => {
|
||||
// AND binds tighter than OR, so this parses as (a AND b AND c) OR d
|
||||
expect(
|
||||
removeKeysFromExpression("a = '1' AND b = '2' AND c = '3' OR d = '4'", [
|
||||
'b',
|
||||
]),
|
||||
).toBe("a = '1' AND c = '3' OR d = '4'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('ANTLR-based removal — deeply nested expressions', () => {
|
||||
const nestedExpr =
|
||||
"((deployment.environment = $env1 OR deployment.environment = 'default') AND service.name = $svc1)";
|
||||
|
||||
it('removes service.name variable — outer and inner single-child parens both drop', () => {
|
||||
// After removal: inner OR group keeps parens (2 items), outer group drops
|
||||
// parens (1 item remains)
|
||||
expect(removeKeysFromExpression(nestedExpr, ['service.name'], true)).toBe(
|
||||
"(deployment.environment = $env1 OR deployment.environment = 'default')",
|
||||
);
|
||||
});
|
||||
|
||||
it('removes deployment.environment variable — inner OR collapses, outer parens kept', () => {
|
||||
// Only the $env1 variable clause is removed; 'default' literal stays.
|
||||
// Inner paren drops (single item left), outer paren stays (2 AND items remain).
|
||||
expect(
|
||||
removeKeysFromExpression(nestedExpr, ['deployment.environment'], true),
|
||||
).toBe("(deployment.environment = 'default' AND service.name = $svc1)");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatValueForExpression', () => {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { CharStreams, CommonTokenStream, ParserRuleContext } from 'antlr4';
|
||||
import { cloneDeep, isEqual, sortBy } from 'lodash-es';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
|
||||
import {
|
||||
DEPRECATED_OPERATORS_MAP,
|
||||
@@ -9,16 +6,7 @@ import {
|
||||
QUERY_BUILDER_FUNCTIONS,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import FilterQueryLexer from 'parser/FilterQueryLexer';
|
||||
import FilterQueryParser, {
|
||||
AndExpressionContext,
|
||||
ComparisonContext,
|
||||
InClauseContext,
|
||||
NotInClauseContext,
|
||||
OrExpressionContext,
|
||||
PrimaryContext,
|
||||
UnaryExpressionContext,
|
||||
} from 'parser/FilterQueryParser';
|
||||
import { cloneDeep, isEqual, sortBy } from 'lodash-es';
|
||||
import { IQueryPair } from 'types/antlrQueryTypes';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
@@ -38,6 +26,7 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import { isQuoted, unquote } from 'utils/stringUtils';
|
||||
import { isFunctionOperator, isNonValueOperator } from 'utils/tokenUtils';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
/**
|
||||
* Check if an operator requires array values (like IN, NOT IN)
|
||||
@@ -524,201 +513,97 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes clauses for specified keys from a filter query expression.
|
||||
* Removes specified key-value pairs from a logical query expression string.
|
||||
*
|
||||
* Uses an ANTLR parse-tree traversal over the existing FilterQuery grammar so that
|
||||
* compound predicates like `BETWEEN x AND y`, `EXISTS`, and `IN (...)` are treated
|
||||
* as atomic nodes — their internal tokens are never confused with top-level AND/OR
|
||||
* conjunctions. Surviving siblings are rejoined with the correct operator at each
|
||||
* level of the tree, producing no dangling operators regardless of expression shape.
|
||||
* If the expression cannot be parsed, it is returned unchanged.
|
||||
* This function parses the given query expression and removes any query pairs
|
||||
* whose keys match those in the `keysToRemove` array. It also removes any trailing
|
||||
* logical conjunctions (e.g., `AND`, `OR`) and whitespace that follow the matched pairs,
|
||||
* ensuring that the resulting expression remains valid and clean.
|
||||
*
|
||||
* @param expression - The full filter query string.
|
||||
* @param keysToRemove - Keys (case-insensitive) whose clauses should be dropped.
|
||||
* @param removeOnlyVariableExpressions - Controls which clauses are eligible for removal:
|
||||
* - `false` (default): removes all clauses for the key regardless of value.
|
||||
* - `true`: removes only the first clause whose value contains any `$`.
|
||||
* - `string` (e.g. `"$service.name"`): removes only the clause whose value exactly
|
||||
* matches that string — preferred when the specific variable reference is known.
|
||||
* @returns The rewritten expression, or an empty string if all clauses were removed.
|
||||
* @param expression - The full query string.
|
||||
* @param keysToRemove - An array of keys (case-insensitive) that should be removed from the expression.
|
||||
* @param removeOnlyVariableExpressions - When true, only removes key-value pairs where the value is a variable (starts with $). When false, uses the original behavior.
|
||||
* @returns A new expression string with the specified keys and their associated clauses removed.
|
||||
*/
|
||||
export const removeKeysFromExpression = (
|
||||
expression: string,
|
||||
keysToRemove: string[],
|
||||
removeOnlyVariableExpressions: string | boolean = false,
|
||||
removeOnlyVariableExpressions = false,
|
||||
): string => {
|
||||
if (!keysToRemove || keysToRemove.length === 0) {
|
||||
return expression;
|
||||
}
|
||||
if (!expression.trim()) {
|
||||
return expression;
|
||||
}
|
||||
|
||||
const keysSet = new Set(keysToRemove.map((k) => k.trim().toLowerCase()));
|
||||
// Tracks keys for which a variable expression has already been removed.
|
||||
// Having multiple $-value clauses for the same key is invalid; we remove at most one.
|
||||
const removedVariableKeys = new Set<string>();
|
||||
let updatedExpression = expression;
|
||||
|
||||
const chars = CharStreams.fromString(expression);
|
||||
const lexer = new FilterQueryLexer(chars);
|
||||
lexer.removeErrorListeners();
|
||||
const tokenStream = new CommonTokenStream(lexer);
|
||||
const parser = new FilterQueryParser(tokenStream);
|
||||
parser.removeErrorListeners();
|
||||
if (updatedExpression) {
|
||||
keysToRemove.forEach((key) => {
|
||||
// Extract key-value query pairs from the expression
|
||||
const existingQueryPairs = extractQueryPairs(updatedExpression);
|
||||
|
||||
const tree = parser.query();
|
||||
let queryPairsMap: Map<string, IQueryPair>;
|
||||
|
||||
// If the expression couldn't be parsed, return it unchanged rather than mangling it
|
||||
if (parser.syntaxErrorsCount > 0) {
|
||||
return expression;
|
||||
}
|
||||
if (existingQueryPairs.length > 0) {
|
||||
// Filter query pairs based on the removeOnlyVariableExpressions flag
|
||||
const filteredQueryPairs = removeOnlyVariableExpressions
|
||||
? existingQueryPairs.filter((pair) => {
|
||||
const pairKey = pair.key?.trim().toLowerCase();
|
||||
const matchesKey = pairKey === `${key}`.trim().toLowerCase();
|
||||
if (!matchesKey) {
|
||||
return false;
|
||||
}
|
||||
const value = pair.value?.toString().trim();
|
||||
return value && value.includes('$');
|
||||
})
|
||||
: existingQueryPairs;
|
||||
|
||||
// Extract original source text for a node, preserving the user's exact formatting
|
||||
const src = (ctx: ParserRuleContext): string =>
|
||||
expression.slice(ctx.start.start, (ctx.stop ?? ctx.start).stop + 1);
|
||||
// Build a map for quick lookup of query pairs by their lowercase trimmed keys
|
||||
queryPairsMap = new Map(
|
||||
filteredQueryPairs.map((pair) => {
|
||||
const key = pair.key.trim().toLowerCase();
|
||||
return [key, pair];
|
||||
}),
|
||||
);
|
||||
|
||||
// Returns null when the entire node should be dropped.
|
||||
// isSingle = true means the result is a single, non-compound expression at
|
||||
// this level (no AND/OR between sibling clauses), which lets the paren
|
||||
// visitor decide whether wrapping is still needed.
|
||||
type VisitResult = { text: string; isSingle: boolean } | null;
|
||||
|
||||
function visitOrExpression(ctx: OrExpressionContext): VisitResult {
|
||||
const parts = ctx
|
||||
.andExpression_list()
|
||||
.map(visitAndExpression)
|
||||
.filter((r): r is NonNullable<VisitResult> => r !== null);
|
||||
if (parts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
// Single surviving branch — pass its isSingle straight through so the
|
||||
// paren visitor can decide whether to keep the outer parens.
|
||||
if (parts.length === 1) {
|
||||
return parts[0];
|
||||
}
|
||||
return { text: parts.map((p) => p.text).join(' OR '), isSingle: false };
|
||||
}
|
||||
|
||||
function visitAndExpression(ctx: AndExpressionContext): VisitResult {
|
||||
const parts = ctx
|
||||
.unaryExpression_list()
|
||||
.map(visitUnaryExpression)
|
||||
.filter((r): r is NonNullable<VisitResult> => r !== null);
|
||||
if (parts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (parts.length === 1) {
|
||||
return { text: parts[0].text, isSingle: true };
|
||||
}
|
||||
return { text: parts.map((p) => p.text).join(' AND '), isSingle: false };
|
||||
}
|
||||
|
||||
function visitUnaryExpression(ctx: UnaryExpressionContext): VisitResult {
|
||||
const primaryResult = visitPrimary(ctx.primary());
|
||||
if (primaryResult === null) {
|
||||
return null;
|
||||
}
|
||||
return ctx.NOT()
|
||||
? { text: `NOT ${primaryResult.text}`, isSingle: true }
|
||||
: primaryResult;
|
||||
}
|
||||
|
||||
function visitPrimary(ctx: PrimaryContext): VisitResult {
|
||||
// Parenthesised sub-expression: ( orExpression )
|
||||
const orCtx = ctx.orExpression();
|
||||
if (orCtx) {
|
||||
const inner = visitOrExpression(orCtx);
|
||||
if (inner === null) {
|
||||
return null;
|
||||
// Lookup the current query pair using the attribute key (case-insensitive)
|
||||
const currentQueryPair = queryPairsMap.get(`${key}`.trim().toLowerCase());
|
||||
if (currentQueryPair && currentQueryPair.isComplete) {
|
||||
// Determine the start index of the query pair (fallback order: key → operator → value)
|
||||
const queryPairStart =
|
||||
currentQueryPair.position.keyStart ??
|
||||
currentQueryPair.position.operatorStart ??
|
||||
currentQueryPair.position.valueStart;
|
||||
// Determine the end index of the query pair (fallback order: value → operator → key)
|
||||
let queryPairEnd =
|
||||
currentQueryPair.position.valueEnd ??
|
||||
currentQueryPair.position.operatorEnd ??
|
||||
currentQueryPair.position.keyEnd;
|
||||
// Get the part of the expression that comes after the current query pair
|
||||
const expressionAfterPair = `${updatedExpression.slice(queryPairEnd + 1)}`;
|
||||
// Match optional spaces and an optional conjunction (AND/OR), case-insensitive
|
||||
const conjunctionOrSpacesRegex = /^(\s*((AND|OR)\s+)?)/i;
|
||||
const match = expressionAfterPair.match(conjunctionOrSpacesRegex);
|
||||
if (match && match.length > 0) {
|
||||
// If match is found, extend the queryPairEnd to include the matched part
|
||||
queryPairEnd += match[0].length;
|
||||
}
|
||||
// Remove the full query pair (including any conjunction/whitespace) from the expression
|
||||
updatedExpression = `${updatedExpression.slice(
|
||||
0,
|
||||
queryPairStart,
|
||||
)}${updatedExpression.slice(queryPairEnd + 1)}`.trim();
|
||||
}
|
||||
}
|
||||
// Drop redundant parens when the group collapses to a single clause;
|
||||
// keep them when multiple clauses remain (operator-precedence matters).
|
||||
if (inner.isSingle) {
|
||||
return { text: inner.text, isSingle: true };
|
||||
}
|
||||
return { text: `(${inner.text})`, isSingle: true };
|
||||
}
|
||||
});
|
||||
|
||||
const compCtx = ctx.comparison();
|
||||
if (compCtx) {
|
||||
const result = visitComparison(compCtx);
|
||||
return result !== null ? { text: result, isSingle: true } : null;
|
||||
}
|
||||
|
||||
// functionCall, fullText, bare key, bare value — keep verbatim
|
||||
return { text: src(ctx), isSingle: true };
|
||||
// Clean up any remaining trailing AND/OR operators and extra whitespace
|
||||
updatedExpression = updatedExpression
|
||||
.replace(/\s+(AND|OR)\s*$/i, '') // Remove trailing AND/OR
|
||||
.replace(/^(AND|OR)\s+/i, '') // Remove leading AND/OR
|
||||
.trim();
|
||||
}
|
||||
|
||||
function visitComparison(ctx: ComparisonContext): string | null {
|
||||
const keyText = ctx.key().getText().trim().toLowerCase();
|
||||
|
||||
if (!keysSet.has(keyText)) {
|
||||
return src(ctx);
|
||||
}
|
||||
|
||||
if (removeOnlyVariableExpressions) {
|
||||
// Scope the value check to value nodes only — not the full comparison text —
|
||||
// so a key that contains '$' does not trigger removal when the value is a
|
||||
// literal. The ANTLR4 runtime returns null from getTypedRuleContext when a
|
||||
// rule is absent, despite the non-nullable TypeScript signatures.
|
||||
const inCtx = ctx.inClause() as unknown as InClauseContext | null;
|
||||
const notInCtx = ctx.notInClause() as unknown as NotInClauseContext | null;
|
||||
// When a specific variable string is supplied, require an exact match so we
|
||||
// never accidentally remove a different $-valued clause for the same key.
|
||||
const matchesVariable = (text: string): boolean =>
|
||||
typeof removeOnlyVariableExpressions === 'string'
|
||||
? text === removeOnlyVariableExpressions
|
||||
: text.includes('$');
|
||||
|
||||
const valueHasVariable = (): boolean => {
|
||||
// Simple comparisons: key = $var, BETWEEN $v1 AND $v2, etc.
|
||||
if (ctx.value_list().some((v) => matchesVariable(v.getText()))) {
|
||||
return true;
|
||||
}
|
||||
// IN $var (bare single value) or IN ($v1, $v2) (value list)
|
||||
if (inCtx) {
|
||||
const bare = inCtx.value() as unknown as { getText(): string } | null;
|
||||
if (bare && matchesVariable(bare.getText())) {
|
||||
return true;
|
||||
}
|
||||
const list = inCtx.valueList() as unknown as {
|
||||
value_list(): { getText(): string }[];
|
||||
} | null;
|
||||
if (list && list.value_list().some((v) => matchesVariable(v.getText()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// NOT IN $var or NOT IN ($v1, $v2)
|
||||
if (notInCtx) {
|
||||
const bare = notInCtx.value() as unknown as { getText(): string } | null;
|
||||
if (bare && matchesVariable(bare.getText())) {
|
||||
return true;
|
||||
}
|
||||
const list = notInCtx.valueList() as unknown as {
|
||||
value_list(): { getText(): string }[];
|
||||
} | null;
|
||||
if (list && list.value_list().some((v) => matchesVariable(v.getText()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (valueHasVariable()) {
|
||||
if (removedVariableKeys.has(keyText)) {
|
||||
return src(ctx);
|
||||
}
|
||||
removedVariableKeys.add(keyText);
|
||||
return null;
|
||||
}
|
||||
return src(ctx);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = visitOrExpression(tree.expression().orExpression());
|
||||
return result?.text ?? '';
|
||||
return updatedExpression;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -360,7 +360,8 @@ describe('createGuardedRoute', () => {
|
||||
const obj = payload[0]?.object;
|
||||
const kind = obj?.resource?.kind;
|
||||
const selector = obj?.selector ?? '*';
|
||||
const objectStr = `${kind}:${selector}`;
|
||||
const objectStr =
|
||||
obj?.resource?.type === 'metaresources' ? kind : `${kind}:${selector}`;
|
||||
requestedObjects.push(objectStr ?? '');
|
||||
|
||||
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { Drawer } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { Maximize2, MessageSquare, Plus, X } from '@signozhq/icons';
|
||||
@@ -52,7 +52,7 @@ export default function AIAssistantDrawer(): JSX.Element {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TooltipSimple title="New conversation">
|
||||
<Tooltip title="New conversation">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -62,9 +62,9 @@ export default function AIAssistantDrawer(): JSX.Element {
|
||||
>
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Open full screen">
|
||||
<Tooltip title="Open full screen">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -75,9 +75,9 @@ export default function AIAssistantDrawer(): JSX.Element {
|
||||
>
|
||||
<Maximize2 size={16} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Close">
|
||||
<Tooltip title="Close">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -87,7 +87,7 @@ export default function AIAssistantDrawer(): JSX.Element {
|
||||
>
|
||||
<X size={16} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { History, Maximize2, Minus, Plus, Sparkles, X } from '@signozhq/icons';
|
||||
|
||||
@@ -132,7 +132,7 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<TooltipSimple title={showHistory ? 'Back to chat' : 'Conversations'}>
|
||||
<Tooltip title={showHistory ? 'Back to chat' : 'Conversations'}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -142,9 +142,9 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
>
|
||||
<History size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="New conversation">
|
||||
<Tooltip title="New conversation">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -153,9 +153,9 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Open full screen">
|
||||
<Tooltip title="Open full screen">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -165,9 +165,9 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Minimize to side panel">
|
||||
<Tooltip title="Minimize to side panel">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -176,9 +176,9 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
>
|
||||
<Minus size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Close">
|
||||
<Tooltip title="Close">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -187,7 +187,7 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { matchPath, useHistory, useLocation } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { History, Maximize2, Plus, Sparkles, X } from '@signozhq/icons';
|
||||
|
||||
@@ -125,7 +125,7 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<TooltipSimple title={showHistory ? 'Back to chat' : 'Conversations'}>
|
||||
<Tooltip title={showHistory ? 'Back to chat' : 'Conversations'}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -135,9 +135,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
>
|
||||
<History size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="New conversation">
|
||||
<Tooltip title="New conversation">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -147,9 +147,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Open full screen">
|
||||
<Tooltip title="Open full screen">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -160,9 +160,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Close">
|
||||
<Tooltip title="Close">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -172,7 +172,7 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { Bot } from '@signozhq/icons';
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function AIAssistantTrigger(): JSX.Element | null {
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipSimple title="AI Assistant">
|
||||
<Tooltip title="AI Assistant">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
@@ -40,6 +40,6 @@ export default function AIAssistantTrigger(): JSX.Element | null {
|
||||
>
|
||||
<Bot size={20} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import cx from 'classnames';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import type { MessageActionDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
|
||||
import {
|
||||
ApplyFilterSignalDTO,
|
||||
@@ -524,9 +524,9 @@ export default function ActionsSection({
|
||||
);
|
||||
|
||||
return tooltip ? (
|
||||
<TooltipSimple key={key} title={tooltip}>
|
||||
<Tooltip key={key} title={tooltip}>
|
||||
{chip}
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span key={key}>{chip}</span>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Badge } from '@signozhq/ui/badge';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import type { UploadFile } from 'antd';
|
||||
import {
|
||||
getListRulesQueryKey,
|
||||
@@ -899,7 +899,7 @@ export default function ChatInput({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<TooltipSimple title="Voice input">
|
||||
<Tooltip title="Voice input">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -910,11 +910,11 @@ export default function ChatInput({
|
||||
>
|
||||
<Mic size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
))}
|
||||
|
||||
{isStreaming && onCancel ? (
|
||||
<TooltipSimple title="Stop generating">
|
||||
<Tooltip title="Stop generating">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="icon"
|
||||
@@ -924,7 +924,7 @@ export default function ChatInput({
|
||||
>
|
||||
<Square size={10} fill="currentColor" strokeWidth={0} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button
|
||||
variant="solid"
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { Plus, Search } from '@signozhq/icons';
|
||||
|
||||
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
|
||||
@@ -157,7 +157,7 @@ export default function ConversationsList({
|
||||
{isLoadingThreads && <HeaderLoadingDots />}
|
||||
|
||||
{!isLoadingThreads && showAddNewConversation && (
|
||||
<TooltipSimple title="New conversation">
|
||||
<Tooltip title="New conversation">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="sm"
|
||||
@@ -167,7 +167,7 @@ export default function ConversationsList({
|
||||
>
|
||||
<Plus size={12} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import cx from 'classnames';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { Check, Copy, RefreshCw, ThumbsDown, ThumbsUp } from '@signozhq/icons';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
@@ -126,7 +126,7 @@ export default function MessageFeedback({
|
||||
<>
|
||||
<div className={cx(styles.feedback, { [styles.visible]: isLastAssistant })}>
|
||||
<div className={styles.actions}>
|
||||
<TooltipSimple title={copied ? 'Copied!' : 'Copy'}>
|
||||
<Tooltip title={copied ? 'Copied!' : 'Copy'}>
|
||||
<Button
|
||||
className={styles.btn}
|
||||
size="icon"
|
||||
@@ -136,9 +136,9 @@ export default function MessageFeedback({
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Good response">
|
||||
<Tooltip title="Good response">
|
||||
<Button
|
||||
className={cx(styles.btn, { [styles.votedUp]: vote === 'positive' })}
|
||||
size="icon"
|
||||
@@ -148,9 +148,9 @@ export default function MessageFeedback({
|
||||
>
|
||||
<ThumbsUp size={12} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Bad response">
|
||||
<Tooltip title="Bad response">
|
||||
<Button
|
||||
className={cx(styles.btn, {
|
||||
[styles.votedDown]: vote === 'negative',
|
||||
@@ -162,10 +162,10 @@ export default function MessageFeedback({
|
||||
>
|
||||
<ThumbsDown size={12} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
{onRegenerate && (
|
||||
<TooltipSimple title="Regenerate">
|
||||
<Tooltip title="Regenerate">
|
||||
<Button
|
||||
className={styles.btn}
|
||||
size="icon"
|
||||
@@ -175,7 +175,7 @@ export default function MessageFeedback({
|
||||
>
|
||||
<RefreshCw size={12} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { Check, Copy } from '@signozhq/icons';
|
||||
|
||||
import { Message } from '../../types';
|
||||
@@ -32,7 +32,7 @@ export default function UserMessageActions({
|
||||
|
||||
return (
|
||||
<div className={styles.actions}>
|
||||
<TooltipSimple title={copied ? 'Copied!' : 'Copy'}>
|
||||
<Tooltip title={copied ? 'Copied!' : 'Copy'}>
|
||||
<Button
|
||||
className={styles.btn}
|
||||
size="icon"
|
||||
@@ -42,7 +42,7 @@ export default function UserMessageActions({
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels';
|
||||
import { updateLocalStorageDashboardVariable } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
@@ -50,7 +48,6 @@ export const useDashboardVariableUpdate =
|
||||
);
|
||||
const addDynamicVariableToPanels = useAddDynamicVariableToPanels();
|
||||
const updateMutation = useUpdateDashboard();
|
||||
const { t } = useTranslation('dashboard');
|
||||
|
||||
const onValueUpdate = useCallback(
|
||||
(
|
||||
@@ -180,14 +177,6 @@ export const useDashboardVariableUpdate =
|
||||
// Get current dashboard variables
|
||||
const currentVariables = dashboardData.data.variables || {};
|
||||
|
||||
const nameExists = Object.values(currentVariables).some(
|
||||
(v) => v.name === name,
|
||||
);
|
||||
if (nameExists) {
|
||||
toast.error(t('variable_name_already_exists', { name, ns: 'dashboard' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create tableRowData like Dashboard Settings does
|
||||
const tableRowData = [];
|
||||
const variableOrderArr = [];
|
||||
@@ -213,20 +202,21 @@ export const useDashboardVariableUpdate =
|
||||
// Create new variable
|
||||
const nextOrder =
|
||||
variableOrderArr.length > 0 ? Math.max(...variableOrderArr) + 1 : 0;
|
||||
const newVariable: IDashboardVariable = {
|
||||
const newVariable: any = {
|
||||
id: uuidv4(),
|
||||
name,
|
||||
type: 'DYNAMIC',
|
||||
type: 'DYNAMIC' as const,
|
||||
description,
|
||||
order: nextOrder,
|
||||
selectedValue: value,
|
||||
allSelected: false,
|
||||
haveCustomValuesSelected: false,
|
||||
sort: 'ASC',
|
||||
sort: 'ASC' as const,
|
||||
multiSelect: true,
|
||||
showALLOption: true,
|
||||
dynamicVariablesAttribute: name,
|
||||
dynamicVariablesSource: source,
|
||||
dynamicVariablesWidgetIds: [],
|
||||
queryValue: '',
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple, TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip, TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
import { Copy } from '@signozhq/icons';
|
||||
import './CopyIconButton.styles.scss';
|
||||
|
||||
@@ -20,7 +20,7 @@ function CopyIconButton({
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipSimple title={tooltipTitle}>
|
||||
<Tooltip title={tooltipTitle}>
|
||||
<span>
|
||||
<Button
|
||||
color="secondary"
|
||||
@@ -33,7 +33,7 @@ function CopyIconButton({
|
||||
onClick={onCopy}
|
||||
/>
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,12 @@ export default {
|
||||
data: {
|
||||
resources: [
|
||||
{
|
||||
kind: 'factor-api-key',
|
||||
type: 'metaresource',
|
||||
kind: 'role',
|
||||
type: 'metaresources',
|
||||
},
|
||||
{
|
||||
kind: 'serviceaccount',
|
||||
type: 'metaresources',
|
||||
},
|
||||
{
|
||||
kind: 'role',
|
||||
@@ -18,13 +22,12 @@ export default {
|
||||
],
|
||||
relations: {
|
||||
assignee: ['role'],
|
||||
attach: ['metaresource', 'role', 'serviceaccount'],
|
||||
create: ['metaresource', 'role', 'serviceaccount'],
|
||||
delete: ['metaresource', 'role', 'serviceaccount'],
|
||||
detach: ['metaresource', 'role', 'serviceaccount'],
|
||||
list: ['metaresource', 'role', 'serviceaccount'],
|
||||
read: ['metaresource', 'role', 'serviceaccount'],
|
||||
update: ['metaresource', 'role', 'serviceaccount'],
|
||||
attach: ['role', 'serviceaccount'],
|
||||
create: ['metaresources'],
|
||||
delete: ['role', 'serviceaccount'],
|
||||
list: ['metaresources'],
|
||||
read: ['role', 'serviceaccount'],
|
||||
update: ['role', 'serviceaccount'],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -80,6 +80,19 @@ export function permissionToTransactionDto(
|
||||
permission: BrandedPermission,
|
||||
): AuthtypesTransactionDTO {
|
||||
const { relation, object: objectStr } = parsePermission(permission);
|
||||
const directType = resolveType(relation, objectStr);
|
||||
if (directType === 'metaresources') {
|
||||
return {
|
||||
relation: relation as AuthtypesRelationDTO,
|
||||
object: {
|
||||
resource: {
|
||||
kind: objectStr as ResourceName,
|
||||
type: directType as CoretypesTypeDTO,
|
||||
},
|
||||
selector: '*',
|
||||
},
|
||||
};
|
||||
}
|
||||
const { resourceName, selector } = splitObjectString(objectStr);
|
||||
const type = resolveType(relation, resourceName) ?? 'metaresource';
|
||||
|
||||
@@ -104,6 +117,9 @@ export function gettableTransactionToPermission(
|
||||
} = item;
|
||||
const resourceName = String(resource.kind);
|
||||
const selectorStr = typeof selector === 'string' ? selector : '*';
|
||||
const objectStr = `${resourceName}${ObjectSeparator}${selectorStr}`;
|
||||
const objectStr =
|
||||
resource.type === 'metaresources'
|
||||
? resourceName
|
||||
: `${resourceName}${ObjectSeparator}${selectorStr}`;
|
||||
return `${relation}${PermissionSeparator}${objectStr}` as BrandedPermission;
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
.body {
|
||||
padding: 12px 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--l1-background);
|
||||
|
||||
// TabsRoot — last direct child div
|
||||
> div:last-child {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[role='tablist'] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Tabs library wrapper — scoped by `.body` so the global match is contained.
|
||||
:global([class*='tabs__list-wrapper']) {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tabsScroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr;
|
||||
gap: 4px 8px;
|
||||
padding: 8px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.serviceName {
|
||||
font-size: 13px;
|
||||
color: var(--l1-foreground);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.barCell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: var(--l3-background);
|
||||
border-radius: 3px;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.barFill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.valueWide {
|
||||
min-width: 55px;
|
||||
}
|
||||
|
||||
.valueNarrow {
|
||||
min-width: 25px;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
.analytics-panel {
|
||||
&__body {
|
||||
padding: 12px 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--l1-background);
|
||||
|
||||
// TabsRoot — last direct child div
|
||||
> div:last-child {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[role='tablist'] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__tabs-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr;
|
||||
gap: 4px 8px;
|
||||
padding: 8px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&__service-name {
|
||||
font-size: 13px;
|
||||
color: var(--l1-foreground);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&__bar-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: var(--l3-background);
|
||||
border-radius: 3px;
|
||||
min-width: 40px;
|
||||
|
||||
&--small {
|
||||
max-width: 80px;
|
||||
flex: 0 0 80px;
|
||||
}
|
||||
}
|
||||
|
||||
&__bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&__value {
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&--wide {
|
||||
min-width: 55px;
|
||||
}
|
||||
|
||||
&--narrow {
|
||||
min-width: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs root
|
||||
[class*='tabs__list-wrapper'] {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
TabsRoot,
|
||||
TabsTrigger,
|
||||
} from '@signozhq/ui/tabs';
|
||||
import cx from 'classnames';
|
||||
import { DetailsHeader } from 'components/DetailsPanel';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
@@ -17,7 +16,7 @@ import {
|
||||
getAggregationMap as findAggregationMap,
|
||||
} from '../../utils/aggregations';
|
||||
|
||||
import styles from './AnalyticsPanel.module.scss';
|
||||
import './AnalyticsPanel.styles.scss';
|
||||
|
||||
interface AnalyticsPanelProps {
|
||||
isOpen: boolean;
|
||||
@@ -87,6 +86,7 @@ function AnalyticsPanel({
|
||||
return (
|
||||
<FloatingPanel
|
||||
isOpen
|
||||
className="analytics-panel"
|
||||
width={PANEL_WIDTH}
|
||||
height={window.innerHeight - PANEL_MARGIN_TOP - PANEL_MARGIN_BOTTOM}
|
||||
defaultPosition={{
|
||||
@@ -110,7 +110,7 @@ function AnalyticsPanel({
|
||||
className="floating-panel__drag-handle"
|
||||
/>
|
||||
|
||||
<div className={styles.body}>
|
||||
<div className="analytics-panel__body">
|
||||
<TabsRoot defaultValue="exec-time">
|
||||
<TabsList variant="secondary">
|
||||
<TabsTrigger value="exec-time" variant="secondary">
|
||||
@@ -121,30 +121,33 @@ function AnalyticsPanel({
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className={styles.tabsScroll}>
|
||||
<div className="analytics-panel__tabs-scroll">
|
||||
<TabsContent value="exec-time">
|
||||
<div className={styles.list}>
|
||||
<div className="analytics-panel__list">
|
||||
{execTimeRows.map((row) => (
|
||||
<>
|
||||
<div
|
||||
key={`${row.group}-dot`}
|
||||
className={styles.dot}
|
||||
className="analytics-panel__dot"
|
||||
style={{ backgroundColor: row.color }}
|
||||
/>
|
||||
<span key={`${row.group}-name`} className={styles.serviceName}>
|
||||
<span
|
||||
key={`${row.group}-name`}
|
||||
className="analytics-panel__service-name"
|
||||
>
|
||||
{row.group}
|
||||
</span>
|
||||
<div key={`${row.group}-bar`} className={styles.barCell}>
|
||||
<div className={styles.bar}>
|
||||
<div key={`${row.group}-bar`} className="analytics-panel__bar-cell">
|
||||
<div className="analytics-panel__bar">
|
||||
<div
|
||||
className={styles.barFill}
|
||||
className="analytics-panel__bar-fill"
|
||||
style={{
|
||||
width: `${Math.min(row.percentage, 100)}%`,
|
||||
backgroundColor: row.color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className={cx(styles.value, styles.valueWide)}>
|
||||
<span className="analytics-panel__value analytics-panel__value--wide">
|
||||
{row.percentage.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
@@ -154,28 +157,31 @@ function AnalyticsPanel({
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="spans">
|
||||
<div className={styles.list}>
|
||||
<div className="analytics-panel__list">
|
||||
{spanCountRows.map((row) => (
|
||||
<>
|
||||
<div
|
||||
key={`${row.group}-dot`}
|
||||
className={styles.dot}
|
||||
className="analytics-panel__dot"
|
||||
style={{ backgroundColor: row.color }}
|
||||
/>
|
||||
<span key={`${row.group}-name`} className={styles.serviceName}>
|
||||
<span
|
||||
key={`${row.group}-name`}
|
||||
className="analytics-panel__service-name"
|
||||
>
|
||||
{row.group}
|
||||
</span>
|
||||
<div key={`${row.group}-bar`} className={styles.barCell}>
|
||||
<div className={styles.bar}>
|
||||
<div key={`${row.group}-bar`} className="analytics-panel__bar-cell">
|
||||
<div className="analytics-panel__bar">
|
||||
<div
|
||||
className={styles.barFill}
|
||||
className="analytics-panel__bar-fill"
|
||||
style={{
|
||||
width: `${(row.count / row.max) * 100}%`,
|
||||
backgroundColor: row.color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className={cx(styles.value, styles.valueNarrow)}>
|
||||
<span className="analytics-panel__value analytics-panel__value--narrow">
|
||||
{row.count}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
.linked-spans {
|
||||
position: relative;
|
||||
|
||||
&__toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
&__label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__chevron {
|
||||
transition: transform 0.15s ease;
|
||||
|
||||
&--open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { Badge } from '@signozhq/ui/badge';
|
||||
import ROUTES from 'constants/routes';
|
||||
import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
||||
|
||||
import styles from './LinkedSpans.module.scss';
|
||||
import './LinkedSpans.styles.scss';
|
||||
|
||||
interface SpanReference {
|
||||
traceId: string;
|
||||
@@ -56,12 +56,12 @@ export function LinkedSpansToggle({
|
||||
toggleOpen: () => void;
|
||||
}): JSX.Element {
|
||||
if (count === 0) {
|
||||
return <span className={styles.label}>0 linked spans</span>;
|
||||
return <span className="linked-spans__label">0 linked spans</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button type="button" className={styles.toggle} onClick={toggleOpen}>
|
||||
<span className={styles.label}>
|
||||
<button type="button" className="linked-spans__toggle" onClick={toggleOpen}>
|
||||
<span className="linked-spans__label">
|
||||
{count} linked span{count !== 1 ? 's' : ''}
|
||||
</span>
|
||||
{isOpen ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
@@ -87,7 +87,7 @@ export function LinkedSpansPanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.list}>
|
||||
<div className="linked-spans__list">
|
||||
{linkedSpans.map((item) => (
|
||||
<KeyValueLabel
|
||||
key={item.spanId}
|
||||
@@ -108,7 +108,7 @@ function LinkedSpans({ references }: LinkedSpansProps): JSX.Element {
|
||||
const { linkedSpans, count, isOpen, toggleOpen } = useLinkedSpans(references);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className="linked-spans">
|
||||
<LinkedSpansToggle count={count} isOpen={isOpen} toggleOpen={toggleOpen} />
|
||||
<LinkedSpansPanel linkedSpans={linkedSpans} isOpen={isOpen} />
|
||||
</div>
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
.root {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
background: var(--l1-background);
|
||||
font-size: 14px;
|
||||
gap: 16px;
|
||||
|
||||
// DataViewer keeps its global `.data-viewer` class — give it a min-height
|
||||
// so the tab area doesn't collapse on short content.
|
||||
:global(.data-viewer) {
|
||||
min-height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.detailsSection {
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tabsSection {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-height: 100%;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
// TabsRoot — direct child of tabs-section
|
||||
> div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[role='tablist'] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[class*='tabs__list-wrapper'] {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tabsScroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
[role='tabpanel'] {
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.spanRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.spanInfo {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 16px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.spanInfoItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--l2-foreground);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.highlightedOptions {
|
||||
padding: 8px 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0;
|
||||
|
||||
// KeyValueLabel uses a global `.key-value-label` root; constrain it
|
||||
// inside the two-column grid so values can ellipsize cleanly.
|
||||
:global(.key-value-label) {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.serviceDot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-forest);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.traceId {
|
||||
color: var(--accent-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.traceIdCopy {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
// Tooltip is rendered in a portal but the SpanDetailsPanel can be docked as a
|
||||
// FloatingPanel (z-index 999), which would otherwise sit on top of the default
|
||||
// tooltip (z-index 50). Bump the tooltip above the panel.
|
||||
.dockToggleTooltip {
|
||||
--tooltip-z-index: 1000;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
.span-details-panel {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
&__header-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
background: var(--l1-background);
|
||||
font-size: 14px;
|
||||
gap: 16px;
|
||||
|
||||
.data-viewer {
|
||||
min-height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
&__details-section {
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__tabs-section {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-height: 100%;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
// TabsRoot — direct child of tabs-section
|
||||
> div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[role='tablist'] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[class*='tabs__list-wrapper'] {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__tabs-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
[role='tabpanel'] {
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__span-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__span-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 16px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
&__span-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--l2-foreground);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__highlighted-options {
|
||||
padding: 8px 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0;
|
||||
|
||||
.key-value-label {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&__service-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-forest);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__trace-id {
|
||||
color: var(--accent-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__trace-id-copy {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&__key-attributes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
|
||||
&-label {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--l2-foreground);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.48px;
|
||||
line-height: var(--line-height-20);
|
||||
}
|
||||
|
||||
&-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip is rendered in a portal but the SpanDetailsPanel can be docked as a
|
||||
// FloatingPanel (z-index 999), which would otherwise sit on top of the default
|
||||
// tooltip (z-index 50). Bump the tooltip above the panel.
|
||||
.dock-toggle-tooltip {
|
||||
--tooltip-z-index: 1000;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
TabsTrigger,
|
||||
} from '@signozhq/ui/tabs';
|
||||
import {
|
||||
TooltipRoot,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
@@ -72,7 +72,7 @@ import SpanPercentileBadge from './SpanPercentile/SpanPercentileBadge';
|
||||
import SpanPercentilePanel from './SpanPercentile/SpanPercentilePanel';
|
||||
import useSpanPercentile from './SpanPercentile/useSpanPercentile';
|
||||
|
||||
import styles from './SpanDetailsPanel.module.scss';
|
||||
import './SpanDetailsPanel.styles.scss';
|
||||
|
||||
interface SpanDetailsPanelProps {
|
||||
panelState: DetailsPanelState;
|
||||
@@ -275,9 +275,9 @@ function SpanDetailsContent({
|
||||
// }, [selectedSpan]);
|
||||
|
||||
return (
|
||||
<div className={styles.body}>
|
||||
<div className={styles.detailsSection}>
|
||||
<div className={styles.spanRow}>
|
||||
<div className="span-details-panel__body">
|
||||
<div className="span-details-panel__details-section">
|
||||
<div className="span-details-panel__span-row">
|
||||
<KeyValueLabel
|
||||
badgeKey="Span name"
|
||||
badgeValue={selectedSpan.name}
|
||||
@@ -296,8 +296,8 @@ function SpanDetailsContent({
|
||||
<SpanPercentilePanel selectedSpan={selectedSpan} percentile={percentile} />
|
||||
|
||||
{/* Span info: exec time + start time */}
|
||||
<div className={styles.spanInfo}>
|
||||
<div className={styles.spanInfoItem}>
|
||||
<div className="span-details-panel__span-info">
|
||||
<div className="span-details-panel__span-info-item">
|
||||
<Timer size={14} />
|
||||
<span>
|
||||
{getYAxisFormattedValue(`${selectedSpan.duration_nano / 1000000}`, 'ms')}
|
||||
@@ -316,13 +316,13 @@ function SpanDetailsContent({
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.spanInfoItem}>
|
||||
<div className="span-details-panel__span-info-item">
|
||||
<CalendarClock size={14} />
|
||||
<span>
|
||||
{dayjs(selectedSpan.timestamp).format('HH:mm:ss — MMM D, YYYY')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.spanInfoItem}>
|
||||
<div className="span-details-panel__span-info-item">
|
||||
<Link2 size={14} />
|
||||
<LinkedSpansToggle
|
||||
count={linkedSpans.count}
|
||||
@@ -338,7 +338,7 @@ function SpanDetailsContent({
|
||||
/>
|
||||
|
||||
{/* Step 6: HighlightedOptions */}
|
||||
<div className={styles.highlightedOptions}>
|
||||
<div className="span-details-panel__highlighted-options">
|
||||
{HIGHLIGHTED_OPTIONS.map((option) => {
|
||||
const rendered = option.render(selectedSpan);
|
||||
if (!rendered) {
|
||||
@@ -382,7 +382,7 @@ function SpanDetailsContent({
|
||||
{/* Step 8: MiniTraceContext */}
|
||||
</div>
|
||||
|
||||
<div className={styles.tabsSection}>
|
||||
<div className="span-details-panel__tabs-section">
|
||||
{/* Step 9: ContentTabs */}
|
||||
<TabsRoot defaultValue="overview">
|
||||
<TabsList variant="secondary">
|
||||
@@ -402,7 +402,7 @@ function SpanDetailsContent({
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
<div className={styles.tabsScroll}>
|
||||
<div className="span-details-panel__tabs-scroll">
|
||||
<TabsContent value="overview">
|
||||
<DataViewer
|
||||
data={spanDisplayData}
|
||||
@@ -497,7 +497,7 @@ function SpanDetailsPanel({
|
||||
key: 'dock-toggle',
|
||||
component: (
|
||||
<TooltipProvider>
|
||||
<TooltipRoot>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -512,10 +512,10 @@ function SpanDetailsPanel({
|
||||
{isDocked ? <Dock size={14} /> : <PanelBottom size={14} />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={styles.dockToggleTooltip}>
|
||||
<TooltipContent className="dock-toggle-tooltip">
|
||||
{isDocked ? 'Open as floating panel' : 'Dock at the bottom'}
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
),
|
||||
});
|
||||
@@ -546,7 +546,7 @@ function SpanDetailsPanel({
|
||||
traceEndTime={traceEndTime}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.body}>
|
||||
<div className="span-details-panel__body">
|
||||
<Skeleton active paragraph={{ rows: 6 }} title={{ width: '60%' }} />
|
||||
</div>
|
||||
)}
|
||||
@@ -554,7 +554,7 @@ function SpanDetailsPanel({
|
||||
);
|
||||
|
||||
if (variant === SpanDetailVariant.DOCKED) {
|
||||
return <div className={styles.root}>{content}</div>;
|
||||
return <div className="span-details-panel">{content}</div>;
|
||||
}
|
||||
|
||||
if (variant === SpanDetailVariant.DRAWER) {
|
||||
@@ -562,7 +562,7 @@ function SpanDetailsPanel({
|
||||
<DetailsPanelDrawer
|
||||
isOpen={panelState.isOpen}
|
||||
onClose={panelState.close}
|
||||
className={styles.root}
|
||||
className="span-details-panel"
|
||||
>
|
||||
{content}
|
||||
</DetailsPanelDrawer>
|
||||
@@ -572,7 +572,7 @@ function SpanDetailsPanel({
|
||||
return (
|
||||
<FloatingPanel
|
||||
isOpen={panelState.isOpen}
|
||||
className={styles.root}
|
||||
className="span-details-panel"
|
||||
width={PANEL_WIDTH}
|
||||
minWidth={480}
|
||||
height={window.innerHeight - PANEL_MARGIN_TOP - PANEL_MARGIN_BOTTOM}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
// Badge — wraps a KeyValueLabel, clickable to toggle panel
|
||||
.span-percentile-badge {
|
||||
cursor: pointer;
|
||||
|
||||
// Override key color for the percentile value (p99)
|
||||
.key-value-label__key {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
&__loader {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 4px;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
&__value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
// Panel — collapsible, renders below the row
|
||||
.span-percentile-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 4px;
|
||||
filter: drop-shadow(2px 4px 16px rgba(0, 0, 0, 0.2));
|
||||
backdrop-filter: blur(20px);
|
||||
margin: 8px 16px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
&-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
|
||||
&-title {
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-20);
|
||||
}
|
||||
|
||||
&-highlight {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
&-loader {
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
margin: 0 4px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&__timerange {
|
||||
width: 100%;
|
||||
|
||||
&-select {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: 50px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__table {
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
&-text {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
&-rows {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&-skeleton {
|
||||
.ant-skeleton-title {
|
||||
width: 100% !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.ant-skeleton-paragraph {
|
||||
margin-top: 8px;
|
||||
|
||||
& > li + li {
|
||||
margin-top: 10px;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 4px;
|
||||
|
||||
&-key {
|
||||
flex: 0 0 auto;
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&-value {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&-dash {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
margin: 0 8px;
|
||||
border-top: 1px solid transparent;
|
||||
border-image: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--l1-border) 0,
|
||||
var(--l1-border) 10px,
|
||||
transparent 10px,
|
||||
transparent 20px
|
||||
)
|
||||
1 stretch;
|
||||
}
|
||||
|
||||
&--current {
|
||||
border-radius: 2px;
|
||||
background: rgba(78, 116, 248, 0.2);
|
||||
|
||||
.span-percentile-panel__table-row-key {
|
||||
color: var(--text-robin-300);
|
||||
}
|
||||
|
||||
.span-percentile-panel__table-row-dash {
|
||||
border-image: repeating-linear-gradient(
|
||||
to right,
|
||||
#abbdff 0,
|
||||
#abbdff 10px,
|
||||
transparent 10px,
|
||||
transparent 20px
|
||||
)
|
||||
1 stretch;
|
||||
}
|
||||
|
||||
.span-percentile-panel__table-row-value {
|
||||
color: var(--text-robin-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__resource-selector {
|
||||
overflow: hidden;
|
||||
width: calc(100% + 16px);
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
left: -8px;
|
||||
z-index: 1000;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
|
||||
&-header {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
&-input {
|
||||
border-radius: 0;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
&-items {
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.3rem;
|
||||
height: 0.3rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
|
||||
&-value {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Badge — wraps a KeyValueLabel, clickable to toggle panel
|
||||
.root {
|
||||
cursor: pointer;
|
||||
|
||||
// KeyValueLabel renders its key with a global class; recolor only the badge
|
||||
// instance inside this badge wrapper.
|
||||
:global(.key-value-label__key) {
|
||||
color: var(--destructive);
|
||||
}
|
||||
}
|
||||
|
||||
.loader {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 4px;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
||||
|
||||
import { UseSpanPercentileReturn } from './useSpanPercentile';
|
||||
|
||||
import styles from './SpanPercentileBadge.module.scss';
|
||||
import './SpanPercentile.styles.scss';
|
||||
|
||||
type SpanPercentileBadgeProps = Pick<
|
||||
UseSpanPercentileReturn,
|
||||
@@ -25,7 +25,7 @@ function SpanPercentileBadge({
|
||||
}: SpanPercentileBadgeProps): JSX.Element | null {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={styles.loader}>
|
||||
<div className="span-percentile-badge__loader">
|
||||
<Loader size={14} className="animate-spin" />
|
||||
</div>
|
||||
);
|
||||
@@ -37,7 +37,7 @@ function SpanPercentileBadge({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.root}
|
||||
className="span-percentile-badge"
|
||||
onClick={toggleOpen}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -50,12 +50,12 @@ function SpanPercentileBadge({
|
||||
<KeyValueLabel
|
||||
badgeKey={`p${percentileValue}`}
|
||||
badgeValue={
|
||||
<span className={styles.value}>
|
||||
<span className="span-percentile-badge__value">
|
||||
{duration}
|
||||
{isOpen ? (
|
||||
<ChevronUp size={14} className={styles.icon} />
|
||||
<ChevronUp size={14} className="span-percentile-badge__icon" />
|
||||
) : (
|
||||
<ChevronDown size={14} className={styles.icon} />
|
||||
<ChevronDown size={14} className="span-percentile-badge__icon" />
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
// Panel — collapsible, renders below the row
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 4px;
|
||||
filter: drop-shadow(2px 4px 16px rgba(0, 0, 0, 0.2));
|
||||
backdrop-filter: blur(20px);
|
||||
margin: 8px 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.contentTitle {
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-20);
|
||||
}
|
||||
|
||||
.contentHighlight {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
.contentLoader {
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
margin: 0 4px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.timerange {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.timerangeSelect {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
:global(.ant-select-selector) {
|
||||
border-radius: 50px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.tableHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tableHeaderText {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.tableRows {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tableSkeleton {
|
||||
:global(.ant-skeleton-title) {
|
||||
width: 100% !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
:global(.ant-skeleton-paragraph) {
|
||||
margin-top: 8px;
|
||||
|
||||
& > li + li {
|
||||
margin-top: 10px;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tableRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.tableRowKey {
|
||||
flex: 0 0 auto;
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.tableRowValue {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.tableRowDash {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
margin: 0 8px;
|
||||
border-top: 1px solid transparent;
|
||||
border-image: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--l1-border) 0,
|
||||
var(--l1-border) 10px,
|
||||
transparent 10px,
|
||||
transparent 20px
|
||||
)
|
||||
1 stretch;
|
||||
}
|
||||
|
||||
.isCurrent {
|
||||
border-radius: 2px;
|
||||
background: rgba(78, 116, 248, 0.2);
|
||||
|
||||
.tableRowKey {
|
||||
color: var(--text-robin-300);
|
||||
}
|
||||
|
||||
.tableRowDash {
|
||||
border-image: repeating-linear-gradient(
|
||||
to right,
|
||||
#abbdff 0,
|
||||
#abbdff 10px,
|
||||
transparent 10px,
|
||||
transparent 20px
|
||||
)
|
||||
1 stretch;
|
||||
}
|
||||
|
||||
.tableRowValue {
|
||||
color: var(--text-robin-400);
|
||||
}
|
||||
}
|
||||
|
||||
.resourceSelector {
|
||||
overflow: hidden;
|
||||
width: calc(100% + 16px);
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
left: -8px;
|
||||
z-index: 1000;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.resourceSelectorHeader {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.resourceSelectorInput {
|
||||
border-radius: 0;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.resourceSelectorItems {
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.3rem;
|
||||
height: 0.3rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
.resourceSelectorItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.resourceSelectorItemValue {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Checkbox, Input, Select, Skeleton } from 'antd';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -10,7 +8,7 @@ import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
|
||||
import { UseSpanPercentileReturn } from './useSpanPercentile';
|
||||
|
||||
import styles from './SpanPercentilePanel.module.scss';
|
||||
import './SpanPercentile.styles.scss';
|
||||
|
||||
const DEFAULT_RESOURCE_ATTRIBUTES = {
|
||||
serviceName: 'service.name',
|
||||
@@ -55,46 +53,46 @@ function SpanPercentilePanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.header}>
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
<div className="span-percentile-panel">
|
||||
<div className="span-percentile-panel__header">
|
||||
<Typography.Text
|
||||
className="span-percentile-panel__header-text"
|
||||
onClick={toggleOpen}
|
||||
prefix={<ChevronDown size={16} />}
|
||||
>
|
||||
Span Percentile
|
||||
</Button>
|
||||
<ChevronDown size={16} /> Span Percentile
|
||||
</Typography.Text>
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
onClick={(): void =>
|
||||
setShowResourceAttributesSelector(!showResourceAttributesSelector)
|
||||
}
|
||||
prefix={
|
||||
showResourceAttributesSelector ? <Check size={16} /> : <Plus size={16} />
|
||||
}
|
||||
/>
|
||||
{showResourceAttributesSelector ? (
|
||||
<Check
|
||||
size={16}
|
||||
className="cursor-pointer span-percentile-panel__header-icon"
|
||||
onClick={(): void => setShowResourceAttributesSelector(false)}
|
||||
/>
|
||||
) : (
|
||||
<Plus
|
||||
size={16}
|
||||
className="cursor-pointer span-percentile-panel__header-icon"
|
||||
onClick={(): void => setShowResourceAttributesSelector(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showResourceAttributesSelector && (
|
||||
<div
|
||||
className={styles.resourceSelector}
|
||||
className="span-percentile-panel__resource-selector"
|
||||
ref={resourceAttributesSelectorRef}
|
||||
>
|
||||
<div className={styles.resourceSelectorHeader}>
|
||||
<div className="span-percentile-panel__resource-selector-header">
|
||||
<Input
|
||||
placeholder="Search resource attributes"
|
||||
className={styles.resourceSelectorInput}
|
||||
className="span-percentile-panel__resource-selector-input"
|
||||
value={resourceAttributesSearchQuery}
|
||||
onChange={(e): void =>
|
||||
setResourceAttributesSearchQuery(e.target.value as string)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.resourceSelectorItems}>
|
||||
<div className="span-percentile-panel__resource-selector-items">
|
||||
{spanResourceAttributes
|
||||
.filter((attr) =>
|
||||
attr.key
|
||||
@@ -102,7 +100,10 @@ function SpanPercentilePanel({
|
||||
.includes(resourceAttributesSearchQuery.toLowerCase()),
|
||||
)
|
||||
.map((attr) => (
|
||||
<div className={styles.resourceSelectorItem} key={attr.key}>
|
||||
<div
|
||||
className="span-percentile-panel__resource-selector-item"
|
||||
key={attr.key}
|
||||
>
|
||||
<Checkbox
|
||||
checked={attr.isSelected}
|
||||
onChange={(e): void => {
|
||||
@@ -117,7 +118,9 @@ function SpanPercentilePanel({
|
||||
attr.key === DEFAULT_RESOURCE_ATTRIBUTES.name
|
||||
}
|
||||
>
|
||||
<div className={styles.resourceSelectorItemValue}>{attr.key}</div>
|
||||
<div className="span-percentile-panel__resource-selector-item-value">
|
||||
{attr.key}
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
@@ -125,15 +128,15 @@ function SpanPercentilePanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.content}>
|
||||
<Typography.Text className={styles.contentTitle}>
|
||||
<div className="span-percentile-panel__content">
|
||||
<Typography.Text className="span-percentile-panel__content-title">
|
||||
This span duration is{' '}
|
||||
{!loading && spanPercentileData ? (
|
||||
<span className={styles.contentHighlight}>
|
||||
<span className="span-percentile-panel__content-highlight">
|
||||
p{Math.floor(spanPercentileData.percentile || 0)}
|
||||
</span>
|
||||
) : (
|
||||
<span className={styles.contentLoader}>
|
||||
<span className="span-percentile-panel__content-loader">
|
||||
<Loader size={12} className="animate-spin" />
|
||||
</span>
|
||||
)}{' '}
|
||||
@@ -141,11 +144,11 @@ function SpanPercentilePanel({
|
||||
hour(s) since the span start time.
|
||||
</Typography.Text>
|
||||
|
||||
<div className={styles.timerange}>
|
||||
<div className="span-percentile-panel__timerange">
|
||||
<Select
|
||||
labelInValue
|
||||
placeholder="Select timerange"
|
||||
className={styles.timerangeSelect}
|
||||
className="span-percentile-panel__timerange-select"
|
||||
getPopupContainer={(trigger): HTMLElement =>
|
||||
trigger.parentElement || document.body
|
||||
}
|
||||
@@ -164,45 +167,45 @@ function SpanPercentilePanel({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={styles.tableHeader}>
|
||||
<Typography.Text className={styles.tableHeaderText}>
|
||||
<div className="span-percentile-panel__table">
|
||||
<div className="span-percentile-panel__table-header">
|
||||
<Typography.Text className="span-percentile-panel__table-header-text">
|
||||
Percentile
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.tableHeaderText}>
|
||||
<Typography.Text className="span-percentile-panel__table-header-text">
|
||||
Duration
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className={styles.tableRows}>
|
||||
<div className="span-percentile-panel__table-rows">
|
||||
{isLoadingData || isFetchingData ? (
|
||||
<Skeleton
|
||||
active
|
||||
paragraph={{ rows: 3 }}
|
||||
className={styles.tableSkeleton}
|
||||
className="span-percentile-panel__table-skeleton"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{Object.entries(spanPercentileData?.percentiles || {}).map(
|
||||
([pKey, pDuration]) => (
|
||||
<div className={styles.tableRow} key={pKey}>
|
||||
<Typography.Text className={styles.tableRowKey}>
|
||||
<div className="span-percentile-panel__table-row" key={pKey}>
|
||||
<Typography.Text className="span-percentile-panel__table-row-key">
|
||||
{pKey}
|
||||
</Typography.Text>
|
||||
<div className={styles.tableRowDash} />
|
||||
<Typography.Text className={styles.tableRowValue}>
|
||||
<div className="span-percentile-panel__table-row-dash" />
|
||||
<Typography.Text className="span-percentile-panel__table-row-value">
|
||||
{getYAxisFormattedValue(`${pDuration / 1000000}`, 'ms')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
|
||||
<div className={cx(styles.tableRow, styles.isCurrent)}>
|
||||
<Typography.Text className={styles.tableRowKey}>
|
||||
<div className="span-percentile-panel__table-row span-percentile-panel__table-row--current">
|
||||
<Typography.Text className="span-percentile-panel__table-row-key">
|
||||
p{Math.floor(spanPercentileData?.percentile || 0)}
|
||||
</Typography.Text>
|
||||
<div className={styles.tableRowDash} />
|
||||
<Typography.Text className={styles.tableRowValue}>
|
||||
<div className="span-percentile-panel__table-row-dash" />
|
||||
<Typography.Text className="span-percentile-panel__table-row-value">
|
||||
(this span){' '}
|
||||
{getYAxisFormattedValue(
|
||||
`${selectedSpan.duration_nano / 1000000}`,
|
||||
|
||||
@@ -5,8 +5,6 @@ import { toast } from '@signozhq/ui/sonner';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
|
||||
import styles from './SpanDetailsPanel.module.scss';
|
||||
|
||||
interface TraceIdFieldProps {
|
||||
span: SpanV3;
|
||||
}
|
||||
@@ -38,7 +36,7 @@ export function TraceIdField({ span }: TraceIdFieldProps): JSX.Element {
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
className={styles.traceIdCopy}
|
||||
className="span-details-panel__trace-id-copy"
|
||||
onClick={handleCopy}
|
||||
title="Click to copy trace ID"
|
||||
>
|
||||
@@ -53,7 +51,7 @@ export function TraceIdField({ span }: TraceIdFieldProps): JSX.Element {
|
||||
pathname: `/trace/${span.trace_id}`,
|
||||
search: window.location.search,
|
||||
}}
|
||||
className={styles.traceId}
|
||||
className="span-details-panel__trace-id"
|
||||
>
|
||||
{span.trace_id}
|
||||
</Link>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ReactNode } from 'react';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
|
||||
import styles from './SpanDetailsPanel.module.scss';
|
||||
import { TraceIdField } from './TraceIdField';
|
||||
|
||||
interface HighlightedOption {
|
||||
@@ -18,7 +17,7 @@ export const HIGHLIGHTED_OPTIONS: HighlightedOption[] = [
|
||||
render: (span): ReactNode | null =>
|
||||
span['service.name'] ? (
|
||||
<Badge color="vanilla">
|
||||
<span className={styles.serviceDot} />
|
||||
<span className="span-details-panel__service-dot" />
|
||||
{span['service.name']}
|
||||
</Badge>
|
||||
) : null,
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
.root {
|
||||
font-size: 12px;
|
||||
color: var(--l1-foreground);
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--l2-foreground);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
color: var(--text-robin-400);
|
||||
}
|
||||
|
||||
.hasError {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 11px;
|
||||
color: var(--l2-foreground);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: 1px solid var(--l2-border);
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.attributes {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.kv {
|
||||
margin-bottom: 2px;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.key {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
.event-tooltip-content {
|
||||
font-size: 12px;
|
||||
color: var(--l1-foreground);
|
||||
max-width: 300px;
|
||||
|
||||
&__header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--l2-foreground);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
color: var(--text-robin-400);
|
||||
|
||||
&.error {
|
||||
color: var(--destructive);
|
||||
}
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: 11px;
|
||||
color: var(--l2-foreground);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&__divider {
|
||||
border-top: 1px solid var(--l2-border);
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
&__attributes {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
&__kv {
|
||||
margin-bottom: 2px;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&__key {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
&__value {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
||||
import { Diamond } from '@signozhq/icons';
|
||||
import cx from 'classnames';
|
||||
import { toFixed } from 'utils/toFixed';
|
||||
|
||||
import styles from './EventTooltipContent.module.scss';
|
||||
import './EventTooltipContent.styles.scss';
|
||||
|
||||
export interface EventTooltipContentProps {
|
||||
eventName: string;
|
||||
@@ -21,25 +20,25 @@ export function EventTooltipContent({
|
||||
const { time, timeUnitName } = convertTimeToRelevantUnit(timeOffsetMs);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.header}>
|
||||
<div className="event-tooltip-content">
|
||||
<div className="event-tooltip-content__header">
|
||||
<Diamond size={10} />
|
||||
<span>EVENT DETAILS</span>
|
||||
</div>
|
||||
<div className={cx(styles.name, isError && styles.hasError)}>
|
||||
<div className={`event-tooltip-content__name ${isError ? 'error' : ''}`}>
|
||||
{eventName}
|
||||
</div>
|
||||
<div className={styles.time}>
|
||||
<div className="event-tooltip-content__time">
|
||||
{toFixed(time, 2)} {timeUnitName} since span start
|
||||
</div>
|
||||
{Object.keys(attributeMap).length > 0 && (
|
||||
<>
|
||||
<div className={styles.divider} />
|
||||
<div className={styles.attributes}>
|
||||
<div className="event-tooltip-content__divider" />
|
||||
<div className="event-tooltip-content__attributes">
|
||||
{Object.entries(attributeMap).map(([key, value]) => (
|
||||
<div key={key} className={styles.kv}>
|
||||
<span className={styles.key}>{key}:</span>{' '}
|
||||
<span className={styles.value}>{value}</span>
|
||||
<div key={key} className="event-tooltip-content__kv">
|
||||
<span className="event-tooltip-content__key">{key}:</span>{' '}
|
||||
<span className="event-tooltip-content__value">{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
// `top` is updated by the parent to track the hovered row's Y; its `left`
|
||||
// is the sidebar/timeline boundary so the popover always opens at the same
|
||||
// X regardless of which row is hovered.
|
||||
.anchor {
|
||||
.span-hover-card-anchor {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.popover {
|
||||
.span-hover-card-popover {
|
||||
// Hover card may be rendered while the SpanDetailsPanel is docked as
|
||||
// a FloatingPanel (z-index 999); bump above the default tooltip z-index.
|
||||
--tooltip-z-index: 1000;
|
||||
@@ -20,29 +20,31 @@
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.content {
|
||||
// Flamegraph tooltip — rendered as a portal, uses same semantic tokens.
|
||||
// Position is set inline on the element (left/top track the cursor); the
|
||||
// static layout/decoration lives here.
|
||||
.flamegraph-tooltip {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
background-color: var(--l1-background);
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
.span-hover-card-content {
|
||||
font-size: 12px;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
&__name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.row {
|
||||
line-height: 1.5;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.preview {
|
||||
// container for additional preview rows
|
||||
}
|
||||
|
||||
.previewKey {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.previewValue {
|
||||
color: var(--l1-foreground);
|
||||
&__row {
|
||||
line-height: 1.5;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
TooltipRoot,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
@@ -11,7 +11,7 @@ import { useMemo } from 'react';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
import { toFixed } from 'utils/toFixed';
|
||||
|
||||
import styles from './SpanHoverCard.module.scss';
|
||||
import './SpanHoverCard.styles.scss';
|
||||
|
||||
/**
|
||||
* Span-level fields that the tooltip always shows (as the colored title or
|
||||
@@ -51,21 +51,27 @@ export function SpanTooltipContent({
|
||||
convertTimeToRelevantUnit(durationMs);
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.name} style={{ color }}>
|
||||
<div className="span-hover-card-content">
|
||||
<div className="span-hover-card-content__name" style={{ color }}>
|
||||
{spanName}
|
||||
</div>
|
||||
<div className={styles.row}>status: {hasError ? 'error' : 'ok'}</div>
|
||||
<div className={styles.row}>start: {toFixed(relativeStartMs, 2)} ms</div>
|
||||
<div className={styles.row}>
|
||||
<div className="span-hover-card-content__row">
|
||||
status: {hasError ? 'error' : 'ok'}
|
||||
</div>
|
||||
<div className="span-hover-card-content__row">
|
||||
start: {toFixed(relativeStartMs, 2)} ms
|
||||
</div>
|
||||
<div className="span-hover-card-content__row">
|
||||
duration: {toFixed(formattedDuration, 2)} {timeUnitName}
|
||||
</div>
|
||||
{previewRows && previewRows.length > 0 && (
|
||||
<div className={styles.preview}>
|
||||
<div className="span-hover-card-content__preview">
|
||||
{previewRows.map((row) => (
|
||||
<div key={row.key} className={styles.row}>
|
||||
<span className={styles.previewKey}>{row.key}:</span>{' '}
|
||||
<span className={styles.previewValue}>{row.value}</span>
|
||||
<div key={row.key} className="span-hover-card-content__row">
|
||||
<span className="span-hover-card-content__preview-key">{row.key}:</span>{' '}
|
||||
<span className="span-hover-card-content__preview-value">
|
||||
{row.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -143,10 +149,10 @@ export function SpanHoverCard({
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipRoot open={hoverCardData !== null} onOpenChange={onOpenChange}>
|
||||
<Tooltip open={hoverCardData !== null} onOpenChange={onOpenChange}>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={styles.anchor}
|
||||
className="span-hover-card-anchor"
|
||||
style={{
|
||||
top: hoverCardData?.anchorTop ?? 0,
|
||||
left: anchorLeft,
|
||||
@@ -158,11 +164,11 @@ export function SpanHoverCard({
|
||||
side="right"
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
className={styles.popover}
|
||||
className="span-hover-card-popover"
|
||||
>
|
||||
{hoverCardData && <SpanTooltipContent {...hoverCardData.tooltip} />}
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
.wrapper {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
gap: 8px;
|
||||
|
||||
// KeyValueLabel renders with a global `.key-value-label` root; keep it from
|
||||
// shrinking on the trace details header.
|
||||
:global(.key-value-label) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.backBtn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filter {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.isExpanded {
|
||||
max-width: none;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.oldViewBtn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.analyticsBtn {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.subHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 4px 16px 8px;
|
||||
font-size: 13px;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.subItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: var(--l2-foreground);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.entryPointBadge {
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
:global(.ant-skeleton-input) {
|
||||
width: 160px !important;
|
||||
min-height: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
.trace-details-header-wrapper {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.trace-details-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
gap: 8px;
|
||||
|
||||
&__back-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.key-value-label {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__filter {
|
||||
&.trace-v3-filter-row {
|
||||
padding: 0;
|
||||
}
|
||||
min-width: 0;
|
||||
|
||||
&--expanded {
|
||||
max-width: none;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__old-view-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__analytics-btn {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&__sub-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 4px 16px 8px;
|
||||
font-size: 13px;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
&__sub-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__separator {
|
||||
color: var(--l2-foreground);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&__entry-point-badge {
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__skeleton .ant-skeleton-input {
|
||||
width: 160px !important;
|
||||
min-height: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,13 @@ import { useCallback, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import {
|
||||
TooltipRoot,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@signozhq/ui/tooltip';
|
||||
import { Skeleton } from 'antd';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import cx from 'classnames';
|
||||
import HttpStatusBadge from 'components/HttpStatusBadge/HttpStatusBadge';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -34,7 +33,7 @@ import AnalyticsPanel from '../SpanDetailsPanel/AnalyticsPanel/AnalyticsPanel';
|
||||
import Filters from '../TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters';
|
||||
import TraceOptionsMenu from './TraceOptionsMenu';
|
||||
|
||||
import styles from './TraceDetailsHeader.module.scss';
|
||||
import './TraceDetailsHeader.styles.scss';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
|
||||
interface FilterMetadata {
|
||||
@@ -69,7 +68,7 @@ function DetailsLoader(): JSX.Element {
|
||||
key={i}
|
||||
active
|
||||
size="small"
|
||||
className={styles.skeleton}
|
||||
className="trace-details-header__skeleton"
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
@@ -121,15 +120,15 @@ function TraceDetailsHeader({
|
||||
convertTimeToRelevantUnit(durationMs);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.header}>
|
||||
<div className="trace-details-header-wrapper">
|
||||
<div className="trace-details-header">
|
||||
{!isFilterExpanded && (
|
||||
<>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="md"
|
||||
className={styles.backBtn}
|
||||
className="trace-details-header__back-btn"
|
||||
onClick={handlePreviousBtnClick}
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
@@ -146,20 +145,20 @@ function TraceDetailsHeader({
|
||||
{!isFilterExpanded && (
|
||||
<>
|
||||
<TooltipProvider>
|
||||
<TooltipRoot>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
className={styles.analyticsBtn}
|
||||
className="trace-details-header__analytics-btn"
|
||||
onClick={(): void => setIsAnalyticsOpen((prev) => !prev)}
|
||||
>
|
||||
<ChartPie size={14} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Analytics</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TraceOptionsMenu
|
||||
showTraceDetails={showTraceDetails}
|
||||
@@ -168,7 +167,11 @@ function TraceDetailsHeader({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className={cx(styles.filter, isFilterExpanded && styles.isExpanded)}>
|
||||
<div
|
||||
className={`trace-details-header__filter${
|
||||
isFilterExpanded ? ' trace-details-header__filter--expanded' : ''
|
||||
}`}
|
||||
>
|
||||
<Filters
|
||||
startTime={filterMetadata.startTime}
|
||||
endTime={filterMetadata.endTime}
|
||||
@@ -184,7 +187,7 @@ function TraceDetailsHeader({
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
className={styles.oldViewBtn}
|
||||
className="trace-details-header__old-view-btn"
|
||||
onClick={handleSwitchToOldView}
|
||||
>
|
||||
Legacy View
|
||||
@@ -195,22 +198,22 @@ function TraceDetailsHeader({
|
||||
</div>
|
||||
|
||||
{showTraceDetails && (
|
||||
<div className={styles.subHeader}>
|
||||
<div className="trace-details-header__sub-header">
|
||||
{traceMetadata ? (
|
||||
<>
|
||||
<span className={styles.subItem}>
|
||||
<span className="trace-details-header__sub-item">
|
||||
<Server size={13} />
|
||||
{traceMetadata.rootServiceName}
|
||||
<span className={styles.separator}>—</span>
|
||||
<span className={styles.entryPointBadge}>
|
||||
<span className="trace-details-header__separator">—</span>
|
||||
<span className="trace-details-header__entry-point-badge">
|
||||
{traceMetadata.rootServiceEntryPoint}
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.subItem}>
|
||||
<span className="trace-details-header__sub-item">
|
||||
<Timer size={13} />
|
||||
{parseFloat(formattedDuration.toFixed(2))} {timeUnitName}
|
||||
</span>
|
||||
<span className={styles.subItem}>
|
||||
<span className="trace-details-header__sub-item">
|
||||
<CalendarClock size={13} />
|
||||
{dayjs(traceMetadata.startTimestampMillis).format(
|
||||
DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS,
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
.root {
|
||||
height: calc(100vh);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Shared Ant Collapse chrome reset for both the flamegraph and waterfall
|
||||
// collapse panels.
|
||||
.flameCollapse,
|
||||
.waterfallCollapse {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
|
||||
:global(.ant-collapse-item) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
:global(.ant-collapse-header) {
|
||||
border-top: 1px solid var(--l2-border);
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
:global(.ant-collapse-content) {
|
||||
background: transparent;
|
||||
border-top: none;
|
||||
// Disable collapse animation — virtualizer and canvas flicker during
|
||||
// height transitions.
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.collapseLabel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.collapseTitle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.collapseCount {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.collapseCountItem {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.hasErrors {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
.flameCollapse {
|
||||
flex-shrink: 0;
|
||||
|
||||
:global(.ant-collapse-content-box) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.waterfallCollapse {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
:global(.ant-collapse-item) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:global(.ant-collapse-content.ant-collapse-content-active) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:global(.ant-collapse-content-box) {
|
||||
padding: 0 !important;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
&.isDocked {
|
||||
flex: none;
|
||||
|
||||
:global(.ant-collapse-item) {
|
||||
flex: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
:global(.ant-collapse-content-box) {
|
||||
flex: none;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dockedSpanDetails {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
124
frontend/src/pages/TraceDetailsV3/TraceDetailsV3.styles.scss
Normal file
124
frontend/src/pages/TraceDetailsV3/TraceDetailsV3.styles.scss
Normal file
@@ -0,0 +1,124 @@
|
||||
.trace-details-v3 {
|
||||
height: calc(100vh);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__flame-collapse,
|
||||
&__waterfall-collapse {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
|
||||
.ant-collapse-item {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ant-collapse-header {
|
||||
border-top: 1px solid var(--l2-border);
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
background: transparent;
|
||||
border-top: none;
|
||||
// Disable collapse animation — virtualizer and canvas flicker during height transitions
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__collapse-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__collapse-count {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
&__collapse-count-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__collapse-count-errors {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
&__flame-collapse {
|
||||
flex-shrink: 0;
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__waterfall-collapse {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-collapse-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ant-collapse-content.ant-collapse-content-active {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 0 !important;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
&--docked {
|
||||
flex: none;
|
||||
|
||||
.ant-collapse-item {
|
||||
flex: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
flex: none;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__docked-span-details {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.viewport {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: block;
|
||||
width: 100%;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Flamegraph tooltip — rendered as a portal, follows the cursor.
|
||||
// Position is set inline on the element (left/top); the layout/decoration
|
||||
// lives here. Shares visual treatment with the waterfall hover popover
|
||||
// but is positioned `fixed` instead of anchored.
|
||||
.tooltip {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
background-color: var(--l1-background);
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--l2-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
.flamegraph-canvas {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.flamegraph-canvas__viewport {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flamegraph-canvas__main {
|
||||
display: block;
|
||||
width: 100%;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.flamegraph-canvas__overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import { useFlamegraphZoom } from './hooks/useFlamegraphZoom';
|
||||
import { useScrollToSpan } from './hooks/useScrollToSpan';
|
||||
import { EventRect, FlamegraphCanvasProps, SpanRect } from './types';
|
||||
|
||||
import styles from './FlamegraphCanvas.module.scss';
|
||||
import './FlamegraphCanvas.styles.scss';
|
||||
|
||||
function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
|
||||
const {
|
||||
@@ -194,7 +194,7 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
|
||||
const tooltipElement = tooltipContent
|
||||
? createPortal(
|
||||
<div
|
||||
className={styles.tooltip}
|
||||
className="span-hover-card-popover flamegraph-tooltip"
|
||||
style={{
|
||||
left: Math.min(tooltipContent.clientX + 15, window.innerWidth - 220),
|
||||
top: Math.min(tooltipContent.clientY + 15, window.innerHeight - 100),
|
||||
@@ -223,7 +223,7 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className="flamegraph-canvas">
|
||||
{tooltipElement}
|
||||
<TimelineV3
|
||||
startTimestamp={viewStartTs}
|
||||
@@ -234,7 +234,7 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
|
||||
/>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={styles.viewport}
|
||||
className="flamegraph-canvas__viewport"
|
||||
onMouseEnter={(): void => {
|
||||
isOverFlamegraphRef.current = true;
|
||||
}}
|
||||
@@ -242,7 +242,7 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={styles.main}
|
||||
className="flamegraph-canvas__main"
|
||||
onMouseDown={(e): void => {
|
||||
handleMouseDown(e);
|
||||
handleMouseDownForClick(e);
|
||||
@@ -251,7 +251,7 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
|
||||
onMouseUp={handleMouseUp}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<canvas ref={overlayCanvasRef} className={styles.overlay} />
|
||||
<canvas ref={overlayCanvasRef} className="flamegraph-canvas__overlay" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
// Container — applied to the `.ant-modal` element via SignozModal's className
|
||||
// prop. Ant Modal portals into document.body, but the className still lives on
|
||||
// the modal root, so descendant overrides work via `:global` nesting.
|
||||
.container {
|
||||
:global(.ant-modal-content),
|
||||
:global(.ant-modal-header) {
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
:global(.ant-modal-header) {
|
||||
border-bottom: none;
|
||||
|
||||
:global(.ant-modal-title) {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-modal-body) {
|
||||
padding: 14px 16px !important;
|
||||
padding-bottom: 0 !important;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
:global(.ant-modal-footer) {
|
||||
margin-top: 0;
|
||||
background: var(--l2-background);
|
||||
border-top: 1px solid var(--l2-border);
|
||||
padding: 16px !important;
|
||||
|
||||
.saveButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
width: 135px;
|
||||
|
||||
:global(.ant-btn-icon) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: var(--l2-foreground);
|
||||
|
||||
:global(.ant-btn-icon svg) {
|
||||
stroke: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.discardButton {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
:global(.ant-btn) {
|
||||
border-radius: 2px;
|
||||
padding: 4px 8px;
|
||||
margin: 0 !important;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inner content wrapper (sibling of modal chrome)
|
||||
.root {
|
||||
// Funnel-detail view overrides — only when the inner wrapper has
|
||||
// `.isDetails` applied alongside `.root`.
|
||||
&.isDetails {
|
||||
:global(.traces-funnel-details) {
|
||||
height: unset;
|
||||
|
||||
:global(.traces-funnel-details__steps-config) {
|
||||
width: unset;
|
||||
border: none;
|
||||
}
|
||||
|
||||
:global(.funnel-step-wrapper) {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
:global(.steps-content) {
|
||||
max-height: 500px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.funnel-item) {
|
||||
padding: 8px 8px 12px 16px;
|
||||
|
||||
&,
|
||||
&:first-child {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
:global(.funnel-item__header) {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
:global(.funnel-item__details) {
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loadingSpinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
background: var(--l3-background);
|
||||
|
||||
:global(.ant-input-prefix) {
|
||||
height: 18px;
|
||||
margin-inline-end: 6px;
|
||||
|
||||
svg {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&,
|
||||
input {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
font-weight: 400;
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: var(--l2-foreground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.createButton {
|
||||
width: 153px;
|
||||
padding: 4px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
background: var(--l3-background);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.list {
|
||||
max-height: 400px;
|
||||
overflow-y: scroll;
|
||||
|
||||
:global(.funnels-empty__content) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.funnels-list) {
|
||||
gap: 8px;
|
||||
|
||||
:global(.funnel-item) {
|
||||
padding: 8px 16px 12px;
|
||||
|
||||
:global(.funnel-item__details) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.backButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.detailsView {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
:global(.funnel-configuration__steps) {
|
||||
padding: 0;
|
||||
|
||||
:global(.funnel-step__content .filters__service-and-span .ant-select) {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
:global(.funnel-step__footer .error) {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
:global(.inter-step-config) {
|
||||
width: calc(100% - 104px);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.funnel-item__actions-popover) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
// Modal base styles
|
||||
.add-span-to-funnel-modal {
|
||||
&__loading-spinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
}
|
||||
&-container {
|
||||
.ant-modal {
|
||||
&-content,
|
||||
&-header {
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
&-header {
|
||||
border-bottom: none;
|
||||
|
||||
.ant-modal-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&-body {
|
||||
padding: 14px 16px !important;
|
||||
padding-bottom: 0 !important;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
margin-top: 0;
|
||||
background: var(--l2-background);
|
||||
border-top: 1px solid var(--l2-border);
|
||||
padding: 16px !important;
|
||||
.add-span-to-funnel-modal {
|
||||
&__save-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
width: 135px;
|
||||
|
||||
.ant-btn-icon {
|
||||
display: flex;
|
||||
}
|
||||
&:disabled {
|
||||
color: var(--l2-foreground);
|
||||
.ant-btn-icon {
|
||||
svg {
|
||||
stroke: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&__discard-button {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
.ant-btn {
|
||||
border-radius: 2px;
|
||||
padding: 4px 8px;
|
||||
margin: 0 !important;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main modal styles
|
||||
.add-span-to-funnel-modal {
|
||||
// Common button styles
|
||||
%button-base {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Details view styles
|
||||
&--details {
|
||||
.traces-funnel-details {
|
||||
height: unset;
|
||||
|
||||
&__steps-config {
|
||||
width: unset;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.funnel-step-wrapper {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.steps-content {
|
||||
max-height: 500px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search section
|
||||
&__search {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
align-items: center;
|
||||
|
||||
&-input {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
background: var(--l3-background);
|
||||
|
||||
.ant-input-prefix {
|
||||
height: 18px;
|
||||
margin-inline-end: 6px;
|
||||
svg {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&,
|
||||
input {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
font-weight: 400;
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: var(--l2-foreground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create button
|
||||
&__create-button {
|
||||
@extend %button-base;
|
||||
width: 153px;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
background: var(--l3-background);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
.funnel-item {
|
||||
padding: 8px 8px 12px 16px;
|
||||
&,
|
||||
&:first-child {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
line-height: 20px;
|
||||
}
|
||||
&__details {
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
// List section
|
||||
&__list {
|
||||
max-height: 400px;
|
||||
overflow-y: scroll;
|
||||
.funnels-empty {
|
||||
&__content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.funnels-list {
|
||||
gap: 8px;
|
||||
|
||||
.funnel-item {
|
||||
padding: 8px 16px 12px;
|
||||
|
||||
&__details {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__spinner {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
// Back button
|
||||
&__back-button {
|
||||
@extend %button-base;
|
||||
gap: 6px;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
// Details section
|
||||
&__details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
.funnel-configuration__steps {
|
||||
padding: 0;
|
||||
|
||||
.funnel-step {
|
||||
&__content .filters__service-and-span .ant-select {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
&__footer .error {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.inter-step-config {
|
||||
width: calc(100% - 104px);
|
||||
}
|
||||
}
|
||||
.funnel-item__actions-popover {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import { filterFunnelsByQuery } from 'pages/TracesFunnels/utils';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
import { FunnelData } from 'types/api/traceFunnels';
|
||||
|
||||
import styles from './AddSpanToFunnelModal.module.scss';
|
||||
import './AddSpanToFunnelModal.styles.scss';
|
||||
|
||||
enum ModalView {
|
||||
LIST = 'list',
|
||||
@@ -62,7 +62,7 @@ function FunnelDetailsView({
|
||||
}, [triggerDiscard, funnel.steps, handleRestoreSteps]);
|
||||
|
||||
return (
|
||||
<div className={styles.detailsView}>
|
||||
<div className="add-span-to-funnel-modal__details">
|
||||
<FunnelListItem
|
||||
funnel={funnel}
|
||||
shouldRedirectToTracesListOnDeleteSuccess={false}
|
||||
@@ -161,11 +161,11 @@ function AddSpanToFunnelModal({
|
||||
};
|
||||
|
||||
const renderListView = (): JSX.Element => (
|
||||
<div className={styles.root}>
|
||||
<div className="add-span-to-funnel-modal">
|
||||
{!!filteredData?.length && (
|
||||
<div className={styles.search}>
|
||||
<div className="add-span-to-funnel-modal__search">
|
||||
<Input
|
||||
className={styles.searchInput}
|
||||
className="add-span-to-funnel-modal__search-input"
|
||||
placeholder="Search by name, description, or tags..."
|
||||
prefix={<Search size={12} />}
|
||||
value={searchQuery}
|
||||
@@ -173,7 +173,7 @@ function AddSpanToFunnelModal({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.list}>
|
||||
<div className="add-span-to-funnel-modal__list">
|
||||
<OverlayScrollbar>
|
||||
<TracesFunnelsContentRenderer
|
||||
isError={isError}
|
||||
@@ -201,11 +201,11 @@ function AddSpanToFunnelModal({
|
||||
);
|
||||
|
||||
const renderDetailsView = ({ span }: { span: SpanV3 }): JSX.Element => (
|
||||
<div className={cx(styles.root, styles.isDetails)}>
|
||||
<div className="add-span-to-funnel-modal add-span-to-funnel-modal--details">
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className={styles.backButton}
|
||||
className="add-span-to-funnel-modal__back-button"
|
||||
onClick={handleBack}
|
||||
prefix={<ArrowLeft size={14} />}
|
||||
>
|
||||
@@ -214,7 +214,7 @@ function AddSpanToFunnelModal({
|
||||
<div className="traces-funnel-details">
|
||||
<div className="traces-funnel-details__steps-config">
|
||||
<Spin
|
||||
className={styles.loadingSpinner}
|
||||
className="add-span-to-funnel-modal__loading-spinner"
|
||||
spinning={isFunnelDetailsLoading || isFunnelDetailsFetching}
|
||||
indicator={<LoaderCircle size={14} className="animate-spin" />}
|
||||
>
|
||||
@@ -245,7 +245,10 @@ function AddSpanToFunnelModal({
|
||||
onCancel={onClose}
|
||||
width={570}
|
||||
title="Add span to funnel"
|
||||
className={styles.container}
|
||||
className={cx('add-span-to-funnel-modal-container', {
|
||||
'add-span-to-funnel-modal-container--details':
|
||||
activeView === ModalView.DETAILS,
|
||||
})}
|
||||
footer={
|
||||
activeView === ModalView.DETAILS
|
||||
? [
|
||||
@@ -254,7 +257,7 @@ function AddSpanToFunnelModal({
|
||||
color="secondary"
|
||||
key="discard"
|
||||
onClick={handleDiscard}
|
||||
className={styles.discardButton}
|
||||
className="add-span-to-funnel-modal__discard-button"
|
||||
disabled={!isUnsavedChanges}
|
||||
>
|
||||
Discard
|
||||
@@ -263,7 +266,7 @@ function AddSpanToFunnelModal({
|
||||
key="save"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
className={styles.saveButton}
|
||||
className="add-span-to-funnel-modal__save-button"
|
||||
onClick={handleSaveFunnel}
|
||||
disabled={!isUnsavedChanges}
|
||||
prefix={<Check size={14} />}
|
||||
@@ -276,7 +279,7 @@ function AddSpanToFunnelModal({
|
||||
key="create"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
className={styles.createButton}
|
||||
className="add-span-to-funnel-modal__create-button"
|
||||
onClick={handleCreateNewClick}
|
||||
prefix={<Plus size={14} />}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.root {
|
||||
.span-line-action-buttons {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
@@ -8,20 +8,20 @@
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
.copyBtn {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 9px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
border-color: var(--l1-border) !important;
|
||||
.copy-span-btn {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 9px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
border-color: var(--l1-border) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip rendered in a portal; bump above FloatingPanel (z-index 999) so it
|
||||
// stays visible when the SpanDetailsPanel is docked as a floating panel.
|
||||
.tooltip {
|
||||
.span-line-action-tooltip {
|
||||
--tooltip-z-index: 1000;
|
||||
}
|
||||
@@ -70,6 +70,24 @@ describe('SpanLineActionButtons', () => {
|
||||
expect(mockOnSpanCopy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('applies correct styling classes', () => {
|
||||
(useCopySpanLink as jest.Mock).mockReturnValue({
|
||||
onSpanCopy: jest.fn(),
|
||||
});
|
||||
|
||||
render(<SpanLineActionButtons span={mockSpan} />);
|
||||
|
||||
// Check if the main container has the correct class
|
||||
const container = screen
|
||||
.getByRole('button')
|
||||
.closest('.span-line-action-buttons');
|
||||
expect(container).toHaveClass('span-line-action-buttons');
|
||||
|
||||
// Check if the button has the correct class
|
||||
const copyButton = screen.getByRole('button');
|
||||
expect(copyButton).toHaveClass('copy-span-btn');
|
||||
});
|
||||
|
||||
it('copies span link to clipboard when copy button is clicked', () => {
|
||||
const mockSetCopy = jest.fn();
|
||||
const mockUrlQuery = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import {
|
||||
TooltipRoot,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
@@ -9,7 +9,7 @@ import { useCopySpanLink } from 'hooks/trace/useCopySpanLink';
|
||||
import { Link } from '@signozhq/icons';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
|
||||
import styles from './SpanLineActionButtons.module.scss';
|
||||
import './SpanLineActionButtons.styles.scss';
|
||||
|
||||
export interface SpanLineActionButtonsProps {
|
||||
span: Span;
|
||||
@@ -20,22 +20,24 @@ export default function SpanLineActionButtons({
|
||||
const { onSpanCopy } = useCopySpanLink(span);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className="span-line-action-buttons">
|
||||
<TooltipProvider>
|
||||
<TooltipRoot>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={onSpanCopy}
|
||||
className={styles.copyBtn}
|
||||
className="copy-span-btn"
|
||||
>
|
||||
<Link size={14} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={styles.tooltip}>Copy Span Link</TooltipContent>
|
||||
</TooltipRoot>
|
||||
<TooltipContent className="span-line-action-tooltip">
|
||||
Copy Span Link
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.root {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.loadingSkeleton {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.trace-waterfall {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.loading-skeleton {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { getVisibleSpans } from './utils';
|
||||
|
||||
import { IInterestedSpan } from './types';
|
||||
|
||||
import styles from './TraceWaterfall.module.scss';
|
||||
import './TraceWaterfall.styles.scss';
|
||||
|
||||
interface ITraceWaterfallProps {
|
||||
traceId: string;
|
||||
@@ -100,7 +100,7 @@ function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element {
|
||||
switch (traceWaterfallState) {
|
||||
case TraceWaterfallStates.LOADING:
|
||||
return (
|
||||
<div className={styles.loadingSkeleton}>
|
||||
<div className="loading-skeleton">
|
||||
<Skeleton active paragraph={{ rows: 6 }} />
|
||||
</div>
|
||||
);
|
||||
@@ -158,7 +158,7 @@ function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element {
|
||||
uncollapsedNodes,
|
||||
]);
|
||||
|
||||
return <div className={styles.root}>{getContent}</div>;
|
||||
return <div className="trace-waterfall">{getContent}</div>;
|
||||
}
|
||||
|
||||
export default TraceWaterfall;
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
.root {
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
margin: 20px;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
border-radius: 4px;
|
||||
background: var(--destructive);
|
||||
}
|
||||
|
||||
.text,
|
||||
.value {
|
||||
color: var(--destructive-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.text {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
.error-waterfall {
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
margin: 20px;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
border-radius: 4px;
|
||||
background: var(--destructive);
|
||||
|
||||
.text {
|
||||
color: var(--destructive-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--destructive-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
import styles from './Error.module.scss';
|
||||
import './Error.styles.scss';
|
||||
|
||||
interface IErrorProps {
|
||||
error: AxiosError;
|
||||
@@ -12,16 +12,10 @@ function Error(props: IErrorProps): JSX.Element {
|
||||
const { error } = props;
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<Typography.Text className={styles.text}>
|
||||
Something went wrong!
|
||||
</Typography.Text>
|
||||
<div className="error-waterfall">
|
||||
<Typography.Text className="text">Something went wrong!</Typography.Text>
|
||||
<Tooltip title={error?.message}>
|
||||
<Typography.Text
|
||||
className={styles.value}
|
||||
title={error?.message}
|
||||
truncate={1}
|
||||
>
|
||||
<Typography.Text className="value" title={error?.message} truncate={1}>
|
||||
{error?.message}
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
// QuerySearch child sets `query-builder-search-v2` globally; size it to the
|
||||
// search container by reaching into the descendant.
|
||||
:global(.query-builder-search-v2) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// ToggleGroup children use generated class names; nest the global selectors
|
||||
// under the local row so they only apply inside this filter row.
|
||||
:global([class*='toggle-group']) {
|
||||
flex-shrink: 0;
|
||||
|
||||
:global([class*='toggle-group-item']) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.isExpanded {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.searchContainer {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
max-width: 220px;
|
||||
min-width: 120px;
|
||||
height: 32px;
|
||||
background: var(--l1-background);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border);
|
||||
}
|
||||
}
|
||||
|
||||
.pillText {
|
||||
font-size: 12px;
|
||||
color: var(--l2-foreground);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pillIndicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-background);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pillPopover {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.pillPopoverHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pillPopoverExpression {
|
||||
font-family: 'Geist Mono', 'Fira Code', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--l1-foreground);
|
||||
word-break: break-all;
|
||||
padding: 6px 8px;
|
||||
background: var(--l2-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.collapseBtn {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.highlightErrorsToggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.preNextToggle {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preNextCount {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: auto;
|
||||
color: var(--l2-foreground);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.filterStatus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
color: var(--l2-foreground);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.hasError {
|
||||
color: var(--destructive);
|
||||
cursor: help;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
.trace-v3-filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
&.expanded {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.filter-search-container {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.query-builder-search-v2 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// --- Collapsed pill ---
|
||||
.filter-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
max-width: 220px;
|
||||
min-width: 120px;
|
||||
height: 32px;
|
||||
background: var(--l1-background);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border);
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 12px;
|
||||
color: var(--l2-foreground);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-background);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Collapsed pill popover ---
|
||||
.filter-pill-popover {
|
||||
max-width: 400px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__expression {
|
||||
font-family: 'Geist Mono', 'Fira Code', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--l1-foreground);
|
||||
word-break: break-all;
|
||||
padding: 6px 8px;
|
||||
background: var(--l2-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// --- ToggleGroup override: size to content, don't stretch items ---
|
||||
[class*='toggle-group'] {
|
||||
flex-shrink: 0;
|
||||
|
||||
[class*='toggle-group-item'] {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Collapse button ---
|
||||
.filter-collapse-btn {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
// --- Highlight errors toggle ---
|
||||
.highlight-errors-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// --- Prev/next navigation ---
|
||||
.pre-next-toggle {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 12px;
|
||||
|
||||
&__count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: auto;
|
||||
color: var(--l2-foreground);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
color: var(--l2-foreground);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
|
||||
&--error {
|
||||
color: var(--destructive);
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,13 @@ import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import {
|
||||
TooltipRoot,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { AxiosError } from 'axios';
|
||||
import cx from 'classnames';
|
||||
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
|
||||
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
@@ -43,7 +42,7 @@ import {
|
||||
useSpanCategoryFilter,
|
||||
} from './hooks/useSpanCategoryFilter';
|
||||
|
||||
import styles from './Filters.module.scss';
|
||||
import './Filters.styles.scss';
|
||||
|
||||
function prepareQuery(filters: TagFilter, traceID: string): Query {
|
||||
return {
|
||||
@@ -256,7 +255,7 @@ function Filters({
|
||||
);
|
||||
|
||||
const highlightErrorsToggle = (
|
||||
<div className={styles.highlightErrorsToggle}>
|
||||
<div className="highlight-errors-toggle">
|
||||
<Typography.Text>Highlight errors</Typography.Text>
|
||||
<Switch
|
||||
color="cherry"
|
||||
@@ -270,9 +269,9 @@ function Filters({
|
||||
<>
|
||||
{isFetching && <Loader className="animate-spin" />}
|
||||
{error && (
|
||||
<TooltipRoot>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className={cx(styles.filterStatus, styles.hasError)}>
|
||||
<span className="filter-status filter-status--error">
|
||||
<Info />
|
||||
API error
|
||||
</span>
|
||||
@@ -280,10 +279,10 @@ function Filters({
|
||||
<TooltipContent>
|
||||
{(error as AxiosError)?.message || 'Something went wrong'}
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!error && noData && (
|
||||
<Typography.Text className={styles.filterStatus}>
|
||||
<Typography.Text className="filter-status">
|
||||
No results found
|
||||
</Typography.Text>
|
||||
)}
|
||||
@@ -294,22 +293,22 @@ function Filters({
|
||||
if (!isExpanded) {
|
||||
const pill = (
|
||||
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
|
||||
<div className={styles.pill} onClick={onExpand}>
|
||||
<div className="filter-pill" onClick={onExpand}>
|
||||
<Search size={12} />
|
||||
<span className={styles.pillText}>{expression || 'Search...'}</span>
|
||||
{expression && <span className={styles.pillIndicator} />}
|
||||
<span className="filter-pill__text">{expression || 'Search...'}</span>
|
||||
{expression && <span className="filter-pill__indicator" />}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className={styles.root}>
|
||||
<div className="trace-v3-filter-row collapsed">
|
||||
{expression ? (
|
||||
<TooltipRoot>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{pill}</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="start">
|
||||
<div className={styles.pillPopover}>
|
||||
<div className={styles.pillPopoverHeader}>
|
||||
<div className="filter-pill-popover">
|
||||
<div className="filter-pill-popover__header">
|
||||
<Typography.Text>Search query</Typography.Text>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -326,10 +325,10 @@ function Filters({
|
||||
<Copy size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.pillPopoverExpression}>{expression}</div>
|
||||
<div className="filter-pill-popover__expression">{expression}</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</Tooltip>
|
||||
) : (
|
||||
pill
|
||||
)}
|
||||
@@ -343,7 +342,7 @@ function Filters({
|
||||
// --- EXPANDED VIEW ---
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className={cx(styles.root, styles.isExpanded)}>
|
||||
<div className="trace-v3-filter-row expanded">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={selectedCategory}
|
||||
@@ -362,7 +361,7 @@ function Filters({
|
||||
</ToggleGroup>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className={styles.searchContainer}
|
||||
className="filter-search-container"
|
||||
ref={containerRef}
|
||||
onBlur={(e): void => {
|
||||
if (!containerRef.current?.contains(e.relatedTarget as Node)) {
|
||||
@@ -383,8 +382,8 @@ function Filters({
|
||||
/>
|
||||
</div>
|
||||
{filteredSpanIds.length > 0 && (
|
||||
<div className={styles.preNextToggle}>
|
||||
<Typography.Text className={styles.preNextCount}>
|
||||
<div className="pre-next-toggle">
|
||||
<Typography.Text className="pre-next-toggle__count">
|
||||
{currentSearchedIndex + 1} / {filteredSpanIds.length}
|
||||
</Typography.Text>
|
||||
<Button
|
||||
@@ -417,7 +416,7 @@ function Filters({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
className={styles.collapseBtn}
|
||||
className="filter-collapse-btn"
|
||||
onClick={onCollapse}
|
||||
>
|
||||
<X size={14} />
|
||||
|
||||
@@ -1,634 +0,0 @@
|
||||
// Inside a .module.scss, postcss-modules auto-scopes `@keyframes` identifiers
|
||||
// and rewrites `animation`/`animation-name` references to match.
|
||||
@keyframes waterfallLoading {
|
||||
0% {
|
||||
opacity: 0.3;
|
||||
transform: scaleX(0.3);
|
||||
transform-origin: left;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scaleX(1);
|
||||
transform-origin: left;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scaleX(0.3);
|
||||
transform-origin: right;
|
||||
}
|
||||
}
|
||||
|
||||
.loadingBar {
|
||||
height: 2px;
|
||||
background: var(--primary);
|
||||
animation: waterfallLoading 1.5s ease-in-out infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Event-dot tooltip. Visually matches SpanHoverCard's popover; styles are
|
||||
// inlined rather than `compose`d from another module because postcss-modules
|
||||
// loads cross-module composes through the plain CSS parser, which chokes on
|
||||
// SCSS `//` comments in the target file.
|
||||
.popover {
|
||||
--tooltip-z-index: 1000;
|
||||
background-color: var(--l1-background);
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--l2-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.root {
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.missingSpans {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 44px;
|
||||
margin: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
background: rgba(69, 104, 220, 0.1);
|
||||
}
|
||||
|
||||
.leftInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.rightInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
color: var(--bg-robin-200);
|
||||
}
|
||||
}
|
||||
|
||||
.splitPanel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 0px 20px 0px 20px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.splitHeader {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
height: 25px;
|
||||
background-color: var(--l1-background);
|
||||
}
|
||||
|
||||
.sidebarHeader {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.resizeHandleHeader {
|
||||
width: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.statusHeader {
|
||||
width: 50px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.timelineHeader {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.splitBody {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Invisible IntersectionObserver targets pinned at the top and bottom of the
|
||||
// virtualized content. See `useBoundaryPagination`.
|
||||
.loadMoreSentinel {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loadMoreSentinelTop {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.loadMoreSentinelBottom {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
|
||||
// ResizableBox child renders with a global `.resizable-box__content` class
|
||||
// — give it independent horizontal scrolling.
|
||||
:global(.resizable-box__content) {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 0.3rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.treeTable {
|
||||
position: relative;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.treeRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.treeCell {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.treeRow:hover,
|
||||
.treeRow.hoveredSpan {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 20%,
|
||||
transparent
|
||||
) !important;
|
||||
|
||||
.spanOverview {
|
||||
background: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarResizeHandle {
|
||||
width: 4px;
|
||||
flex-shrink: 0;
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
background: transparent;
|
||||
transition: background 0.15s ease;
|
||||
z-index: 1;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background: rgba(35, 196, 248, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.statusCol {
|
||||
width: 50px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.statusCell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
--badge-border-radius: 2px;
|
||||
--badge-padding: 3px 6px;
|
||||
--badge-line-height: 12px;
|
||||
--badge-border-width: 0px;
|
||||
|
||||
&.hoveredSpan {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 20%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
&.isInterested,
|
||||
&.isSelectedNonMatching {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 40%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
&.isDimmed {
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.crosshair {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: var(--l3-background);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.timelineRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// Match timelineHeader's 15px padding so bars align with ticks
|
||||
padding: 0 15px;
|
||||
|
||||
&:hover,
|
||||
&.hoveredSpan {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 20%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
&:has(.isInterested),
|
||||
&:has(.isSelectedNonMatching) {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 40%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.spanOverview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover .rowActions {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&.isInterested,
|
||||
&.isSelectedNonMatching {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 40%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.treeIndent {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.treeLine {
|
||||
position: absolute;
|
||||
background-color: var(--l2-border);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.treeConnector {
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 50%;
|
||||
border-left: 1px solid var(--l2-border);
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
border-bottom-left-radius: 6px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Reserved horizontal space for the chevron — present on every row, filled
|
||||
// only when the span has children. Keeps sibling icons aligned.
|
||||
.treeArrowSlot {
|
||||
width: 18px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.treeArrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
color: var(--l1-foreground);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
// Reserved horizontal space for the subtree-count badge — same reason.
|
||||
// Right-aligns the badge inside so single-digit counts don't push the icon
|
||||
// left of where multi-digit counts would put it.
|
||||
.subtreeCountSlot {
|
||||
min-width: 34px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.subtreeCount {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
:global(.badge) {
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
padding: 0 4px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.treeIcon {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
margin: 0 6px;
|
||||
|
||||
&.hasError {
|
||||
box-shadow: 0 0 0 2px rgba(255, 70, 70, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.treeLabel {
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Inter';
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 28px;
|
||||
letter-spacing: 0.01em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.treeServiceName {
|
||||
margin-left: 18px;
|
||||
color: var(--l3-foreground);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.rowActions {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-left: auto;
|
||||
padding-right: 4px;
|
||||
padding-left: 8px;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
background: linear-gradient(to left, var(--l1-background) 60%, transparent);
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.actionBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
color: var(--l1-foreground);
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
// Also reveal the actions when the parent tree row is hovered.
|
||||
.treeRow:hover .rowActions,
|
||||
.treeRow.hoveredSpan .rowActions {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.spanDuration {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 0 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.spanBar {
|
||||
position: absolute;
|
||||
height: 18px;
|
||||
top: 5px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
background-color: var(--span-color);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.spanInfo {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.spanName {
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.spanDurationText {
|
||||
color: inherit;
|
||||
opacity: 0.8;
|
||||
font-size: 10px;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.eventDot {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background-color: var(--event-dot-bg, var(--bg-robin-500));
|
||||
border: 1px solid var(--event-dot-border, var(--bg-robin-600));
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
|
||||
&.hasError {
|
||||
background-color: var(--destructive);
|
||||
border-color: var(--destructive);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translate(-50%, -50%) rotate(45deg) scale(1.5);
|
||||
}
|
||||
}
|
||||
|
||||
// Hover state: solid l2-background fill + border (matches flamegraph)
|
||||
.timelineRow:hover .spanBar,
|
||||
.timelineRow.hoveredSpan .spanBar {
|
||||
color: var(--span-color);
|
||||
background-color: var(--l2-background);
|
||||
background-image: none;
|
||||
border: 1px solid var(--span-color);
|
||||
}
|
||||
|
||||
// Selected state: solid l3-background fill + dashed border (matches flamegraph)
|
||||
.isInterested .spanBar,
|
||||
.isSelectedNonMatching .spanBar {
|
||||
color: var(--span-color);
|
||||
background-color: var(--l2-background);
|
||||
background-image: none;
|
||||
border: 1px dashed var(--span-color);
|
||||
}
|
||||
|
||||
.isDimmed {
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
.isHighlighted {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.isSelectedNonMatching {
|
||||
.treeLabel {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
// `.spanBar` text color is the one place where semantic tokens don't fit
|
||||
// cleanly: in dark mode the bar's bright `--span-color` background needs dark
|
||||
// text; in light mode `generateColor` produces darker bar fills, so the text
|
||||
// must flip to white.
|
||||
:global(.lightMode) {
|
||||
.root {
|
||||
.spanDuration .spanBar {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.timelineRow:hover .spanBar,
|
||||
.timelineRow.hoveredSpan .spanBar,
|
||||
.isInterested .spanBar,
|
||||
.isSelectedNonMatching .spanBar {
|
||||
color: var(--span-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltips for the row's hover-revealed action buttons (Copy / Add to Funnel).
|
||||
// Bumped above FloatingPanel (z-index 999) so they stay visible when the
|
||||
// SpanDetailsPanel is docked as a floating panel.
|
||||
.actionTooltip {
|
||||
--tooltip-z-index: 1000;
|
||||
}
|
||||
@@ -0,0 +1,636 @@
|
||||
.waterfall-loading-bar {
|
||||
height: 2px;
|
||||
background: var(--primary);
|
||||
animation: waterfall-loading 1.5s ease-in-out infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes waterfall-loading {
|
||||
0% {
|
||||
opacity: 0.3;
|
||||
transform: scaleX(0.3);
|
||||
transform-origin: left;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scaleX(1);
|
||||
transform-origin: left;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scaleX(0.3);
|
||||
transform-origin: right;
|
||||
}
|
||||
}
|
||||
|
||||
.success-content {
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.missing-spans {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 44px;
|
||||
margin: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
background: rgba(69, 104, 220, 0.1);
|
||||
|
||||
.left-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
.text {
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
|
||||
.right-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.right-info:hover {
|
||||
background-color: unset;
|
||||
color: var(--bg-robin-200);
|
||||
}
|
||||
}
|
||||
|
||||
.waterfall-split-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 0px 20px 0px 20px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.waterfall-split-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
height: 25px;
|
||||
background-color: var(--l1-background);
|
||||
|
||||
.sidebar-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.resize-handle-header {
|
||||
width: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-header {
|
||||
width: 50px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.waterfall-split-body {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Invisible IntersectionObserver targets pinned at the top and bottom of
|
||||
// the virtualized content. See `useBoundaryPagination`.
|
||||
.waterfall-load-more-sentinel {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
pointer-events: none;
|
||||
|
||||
&--top {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&--bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.waterfall-sidebar {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
|
||||
.resizable-box__content {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 0.3rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.span-tree-table {
|
||||
position: relative;
|
||||
border-collapse: collapse;
|
||||
|
||||
.span-tree-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.span-tree-cell {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.span-tree-row:hover,
|
||||
.span-tree-row.hovered-span {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 20%,
|
||||
transparent
|
||||
) !important;
|
||||
|
||||
.span-overview {
|
||||
background: unset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-resize-handle {
|
||||
width: 4px;
|
||||
flex-shrink: 0;
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
background: transparent;
|
||||
transition: background 0.15s ease;
|
||||
z-index: 1;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background: rgba(35, 196, 248, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.waterfall-status-col {
|
||||
width: 50px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
|
||||
.status-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
--badge-border-radius: 2px;
|
||||
--badge-padding: 3px 6px;
|
||||
--badge-line-height: 12px;
|
||||
--badge-border-width: 0px;
|
||||
|
||||
&.hovered-span {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 20%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
&.interested-span,
|
||||
&.selected-non-matching-span {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 40%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
&.dimmed-span {
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.waterfall-timeline {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.waterfall-crosshair {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: var(--l3-background);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.timeline-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// Match timeline-header's 15px padding so bars align with ticks
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.timeline-row:hover,
|
||||
.timeline-row.hovered-span {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 20%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
.timeline-row:has(.interested-span),
|
||||
.timeline-row:has(.selected-non-matching-span) {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 40%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Shared span component styles (used in both panels)
|
||||
.span-overview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
|
||||
.tree-indent {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tree-line {
|
||||
position: absolute;
|
||||
background-color: var(--l2-border);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tree-connector {
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 50%;
|
||||
border-left: 1px solid var(--l2-border);
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
border-bottom-left-radius: 6px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Reserved horizontal space for the chevron — present on every row,
|
||||
// filled only when the span has children. Keeps sibling icons aligned.
|
||||
.tree-arrow-slot {
|
||||
width: 18px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tree-arrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
color: var(--l1-foreground);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
// Reserved horizontal space for the subtree-count badge — same reason.
|
||||
// Right-aligns the badge inside so single-digit counts don't push the
|
||||
// icon left of where multi-digit counts would put it.
|
||||
.subtree-count-slot {
|
||||
min-width: 34px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.subtree-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.badge {
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
padding: 0 4px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-icon {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
margin: 0 6px;
|
||||
|
||||
&.is-error {
|
||||
box-shadow: 0 0 0 2px rgba(255, 70, 70, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.tree-label {
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Inter';
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 28px;
|
||||
letter-spacing: 0.01em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.tree-service-name {
|
||||
margin-left: 18px;
|
||||
color: var(--l3-foreground);
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.span-row-actions {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-left: auto;
|
||||
padding-right: 4px;
|
||||
padding-left: 8px;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
background: linear-gradient(to left, var(--l1-background) 60%, transparent);
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
.span-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
color: var(--l1-foreground);
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .span-row-actions {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Also show action buttons when hovering the tree row (parent of span-overview)
|
||||
.span-tree-row:hover .span-row-actions,
|
||||
.span-tree-row.hovered-span .span-row-actions {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.span-duration {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 0 15px;
|
||||
cursor: pointer;
|
||||
|
||||
.span-bar {
|
||||
position: absolute;
|
||||
height: 18px;
|
||||
top: 5px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
background-color: var(--span-color);
|
||||
border: 1px solid transparent;
|
||||
|
||||
.span-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
|
||||
.span-name {
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.span-duration-text {
|
||||
color: inherit;
|
||||
opacity: 0.8;
|
||||
font-size: 10px;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.event-dot {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background-color: var(--event-dot-bg, var(--bg-robin-500));
|
||||
border: 1px solid var(--event-dot-border, var(--bg-robin-600));
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
|
||||
&.error {
|
||||
background-color: var(--destructive);
|
||||
border-color: var(--destructive);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translate(-50%, -50%) rotate(45deg) scale(1.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hover state: solid l2-background fill + border (matches flamegraph)
|
||||
.timeline-row:hover .span-bar,
|
||||
.timeline-row.hovered-span .span-bar {
|
||||
color: var(--span-color);
|
||||
background-color: var(--l2-background);
|
||||
background-image: none;
|
||||
border: 1px solid var(--span-color);
|
||||
}
|
||||
|
||||
// Selected state: solid l3-background fill + dashed border (matches flamegraph)
|
||||
.interested-span .span-bar,
|
||||
.selected-non-matching-span .span-bar {
|
||||
color: var(--span-color);
|
||||
background-color: var(--l2-background);
|
||||
background-image: none;
|
||||
border: 1px dashed var(--span-color);
|
||||
}
|
||||
|
||||
// Shared state classes for both panels
|
||||
// Background highlight for selection is on .timeline-row via :has() — see .waterfall-timeline
|
||||
// Only apply on .span-overview (left panel) where there's no parent row with :has()
|
||||
.span-overview.interested-span,
|
||||
.span-overview.selected-non-matching-span {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 40%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
.dimmed-span {
|
||||
opacity: 0.15;
|
||||
}
|
||||
.highlighted-span {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.selected-non-matching-span {
|
||||
.tree-label {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.span-dets {
|
||||
.related-logs {
|
||||
display: flex;
|
||||
width: 160px;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// `.span-bar` text color is the one place where semantic tokens don't fit
|
||||
// cleanly: in dark mode the bar's bright `--span-color` background needs dark
|
||||
// text; in light mode `generateColor` produces darker bar fills, so the text
|
||||
// must flip to white. This is the only `.lightMode` carve-out left after the
|
||||
// migration to semantic tokens — the hover/selected rules are repeated here
|
||||
// to beat the default-state rule's specificity inside `.lightMode`.
|
||||
.lightMode {
|
||||
.success-content {
|
||||
.span-duration .span-bar {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.timeline-row:hover .span-bar,
|
||||
.timeline-row.hovered-span .span-bar,
|
||||
.interested-span .span-bar,
|
||||
.selected-non-matching-span .span-bar {
|
||||
color: var(--span-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltips for the row's hover-revealed action buttons (Copy / Add to Funnel).
|
||||
// Bumped above FloatingPanel (z-index 999) so they stay visible when the
|
||||
// SpanDetailsPanel is docked as a floating panel.
|
||||
.span-action-tooltip {
|
||||
--tooltip-z-index: 1000;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import {
|
||||
TooltipRoot,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
@@ -53,7 +53,7 @@ import { SpanHoverCard } from '../../../SpanHoverCard/SpanHoverCard';
|
||||
import AddSpanToFunnelModal from '../../AddSpanToFunnelModal/AddSpanToFunnelModal';
|
||||
import { IInterestedSpan } from '../../types';
|
||||
|
||||
import styles from './Success.module.scss';
|
||||
import './Success.styles.scss';
|
||||
|
||||
/**
|
||||
* Lazy event dot — only mounts the tooltip when the user hovers.
|
||||
@@ -91,7 +91,7 @@ const LazyEventDotPopover = memo(function LazyEventDotPopover({
|
||||
|
||||
const dot = (
|
||||
<div
|
||||
className={cx(styles.eventDot, isError && styles.hasError)}
|
||||
className={`event-dot ${isError ? 'error' : ''}`}
|
||||
style={
|
||||
{
|
||||
left: `${dotLeft}%`,
|
||||
@@ -112,16 +112,16 @@ const LazyEventDotPopover = memo(function LazyEventDotPopover({
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipRoot
|
||||
<Tooltip
|
||||
open
|
||||
onOpenChange={(open: boolean): void => {
|
||||
onOpenChange={(open): void => {
|
||||
if (!open) {
|
||||
setShowPopover(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TooltipTrigger asChild>{dot}</TooltipTrigger>
|
||||
<TooltipContent className={styles.popover}>
|
||||
<TooltipContent className="span-hover-card-popover">
|
||||
<EventTooltipContent
|
||||
eventName={event.name}
|
||||
timeOffsetMs={eventTimeMs - spanTimestamp}
|
||||
@@ -129,7 +129,7 @@ const LazyEventDotPopover = memo(function LazyEventDotPopover({
|
||||
attributeMap={event.attributeMap || {}}
|
||||
/>
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
});
|
||||
@@ -243,11 +243,11 @@ const SpanOverview = memo(function SpanOverview({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.spanOverview, {
|
||||
[styles.isInterested]: isSelected && (!isFilterActive || isMatching),
|
||||
[styles.isHighlighted]: isHighlighted,
|
||||
[styles.isSelectedNonMatching]: isSelectedNonMatching,
|
||||
[styles.isDimmed]: isDimmed,
|
||||
className={cx('span-overview', {
|
||||
'interested-span': isSelected && (!isFilterActive || isMatching),
|
||||
'highlighted-span': isHighlighted,
|
||||
'selected-non-matching-span': isSelectedNonMatching,
|
||||
'dimmed-span': isDimmed,
|
||||
})}
|
||||
onClick={(): void => handleSpanClick(span)}
|
||||
onMouseEnter={(): void => onHoverEnter(span.span_id)}
|
||||
@@ -264,7 +264,7 @@ const SpanOverview = memo(function SpanOverview({
|
||||
return (
|
||||
<div
|
||||
key={lvl}
|
||||
className={styles.treeLine}
|
||||
className="tree-line"
|
||||
style={{
|
||||
left: xPos,
|
||||
top: 0,
|
||||
@@ -277,25 +277,25 @@ const SpanOverview = memo(function SpanOverview({
|
||||
return (
|
||||
<div key={lvl}>
|
||||
<div
|
||||
className={styles.treeLine}
|
||||
className="tree-line"
|
||||
style={{ left: xPos, top: 0, width: 1, height: '50%' }}
|
||||
/>
|
||||
<div className={styles.treeConnector} style={{ left: xPos, top: 0 }} />
|
||||
<div className="tree-connector" style={{ left: xPos, top: 0 }} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Indent spacer */}
|
||||
<span className={styles.treeIndent} style={{ width: `${indentWidth}px` }} />
|
||||
<span className="tree-indent" style={{ width: `${indentWidth}px` }} />
|
||||
|
||||
{/* Expand/collapse arrow + child count slots — always render the
|
||||
slots, fill them only when the span has children. Reserving the
|
||||
horizontal space on leaf rows aligns sibling icons regardless
|
||||
of whether each sibling is a parent or a leaf. */}
|
||||
<span className={styles.treeArrowSlot}>
|
||||
<span className="tree-arrow-slot">
|
||||
{span.has_children && (
|
||||
<span
|
||||
className={styles.treeArrow}
|
||||
className={cx('tree-arrow', { expanded: !isSpanCollapsed })}
|
||||
onClick={(event): void => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
@@ -306,9 +306,9 @@ const SpanOverview = memo(function SpanOverview({
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className={styles.subtreeCountSlot}>
|
||||
<span className="subtree-count-slot">
|
||||
{span.has_children && (
|
||||
<span className={styles.subtreeCount}>
|
||||
<span className="subtree-count">
|
||||
<Badge color="vanilla">{span.sub_tree_node_count}</Badge>
|
||||
</span>
|
||||
)}
|
||||
@@ -316,51 +316,51 @@ const SpanOverview = memo(function SpanOverview({
|
||||
|
||||
{/* Colored service dot */}
|
||||
<span
|
||||
className={cx(styles.treeIcon, { [styles.hasError]: span.has_error })}
|
||||
className={cx('tree-icon', { 'is-error': span.has_error })}
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
|
||||
{/* Span name + service name */}
|
||||
<span className={styles.treeLabel}>
|
||||
<span className="tree-label">
|
||||
{span.name}
|
||||
<span className={styles.treeServiceName}>{span['service.name']}</span>
|
||||
<span className="tree-service-name">{span['service.name']}</span>
|
||||
</span>
|
||||
|
||||
{/* Action buttons — shown on hover via CSS, right-aligned */}
|
||||
<span className={styles.rowActions}>
|
||||
<span className="span-row-actions">
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<TooltipRoot>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
className={styles.actionBtn}
|
||||
className="span-action-btn"
|
||||
onClick={onSpanCopy}
|
||||
>
|
||||
<Link size={12} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={styles.actionTooltip}>
|
||||
<TooltipContent className="span-action-tooltip">
|
||||
Copy Span Link
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
<TooltipRoot>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
className={styles.actionBtn}
|
||||
className="span-action-btn"
|
||||
onClick={handleFunnelClick}
|
||||
>
|
||||
<ListPlus size={12} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={styles.actionTooltip}>
|
||||
<TooltipContent className="span-action-tooltip">
|
||||
Add to Trace Funnel
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</span>
|
||||
</div>
|
||||
@@ -410,16 +410,16 @@ export const SpanDuration = memo(function SpanDuration({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.spanDuration, {
|
||||
[styles.isInterested]: isSelected && (!isFilterActive || isMatching),
|
||||
[styles.isHighlighted]: isHighlighted,
|
||||
[styles.isSelectedNonMatching]: isSelectedNonMatching,
|
||||
[styles.isDimmed]: isDimmed,
|
||||
className={cx('span-duration', {
|
||||
'interested-span': isSelected && (!isFilterActive || isMatching),
|
||||
'highlighted-span': isHighlighted,
|
||||
'selected-non-matching-span': isSelectedNonMatching,
|
||||
'dimmed-span': isDimmed,
|
||||
})}
|
||||
onClick={(): void => handleSpanClick(span)}
|
||||
>
|
||||
<div
|
||||
className={styles.spanBar}
|
||||
className="span-bar"
|
||||
style={
|
||||
{
|
||||
left: `${leftOffset}%`,
|
||||
@@ -429,9 +429,9 @@ export const SpanDuration = memo(function SpanDuration({
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<span className={styles.spanInfo}>
|
||||
<span className={styles.spanName}>{span.name}</span>
|
||||
<span className={styles.spanDurationText}>{`${toFixed(
|
||||
<span className="span-info">
|
||||
<span className="span-name">{span.name}</span>
|
||||
<span className="span-duration-text">{`${toFixed(
|
||||
time,
|
||||
2,
|
||||
)} ${timeUnitName}`}</span>
|
||||
@@ -529,11 +529,11 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
|
||||
if (prev) {
|
||||
const prevElements = document.querySelectorAll(`[data-span-id="${prev}"]`);
|
||||
prevElements.forEach((el) => el.classList.remove(styles.hoveredSpan));
|
||||
prevElements.forEach((el) => el.classList.remove('hovered-span'));
|
||||
}
|
||||
if (spanId) {
|
||||
const nextElements = document.querySelectorAll(`[data-span-id="${spanId}"]`);
|
||||
nextElements.forEach((el) => el.classList.add(styles.hoveredSpan));
|
||||
nextElements.forEach((el) => el.classList.add('hovered-span'));
|
||||
}
|
||||
prevHoveredSpanIdRef.current = spanId;
|
||||
}, []);
|
||||
@@ -798,17 +798,17 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className="success-content">
|
||||
{traceMetadata.hasMissingSpans && (
|
||||
<div className={styles.missingSpans}>
|
||||
<section className={styles.leftInfo}>
|
||||
<div className="missing-spans">
|
||||
<section className="left-info">
|
||||
<CircleAlert size={14} />
|
||||
<span className={styles.text}>This trace has missing spans</span>
|
||||
<span className="text">This trace has missing spans</span>
|
||||
</section>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className={styles.rightInfo}
|
||||
className="right-info"
|
||||
suffix={<ArrowUpRight size={14} />}
|
||||
onClick={(): WindowProxy | null =>
|
||||
window.open(
|
||||
@@ -821,17 +821,17 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isFetching && <div className={styles.loadingBar} />}
|
||||
<div className={styles.splitPanel} ref={scrollContainerRef}>
|
||||
{isFetching && <div className="waterfall-loading-bar" />}
|
||||
<div className="waterfall-split-panel" ref={scrollContainerRef}>
|
||||
{/* Sticky header row */}
|
||||
<div className={styles.splitHeader}>
|
||||
<div className="waterfall-split-header">
|
||||
<div
|
||||
className={styles.sidebarHeader}
|
||||
className="sidebar-header"
|
||||
style={{ width: sidebarWidth, flexShrink: 0 }}
|
||||
/>
|
||||
<div className={styles.resizeHandleHeader} />
|
||||
<div className={styles.statusHeader} />
|
||||
<div className={styles.timelineHeader}>
|
||||
<div className="resize-handle-header" />
|
||||
<div className="status-header" />
|
||||
<div className="timeline-header">
|
||||
<TimelineV3
|
||||
startTimestamp={traceMetadata.startTime}
|
||||
endTimestamp={traceMetadata.endTime}
|
||||
@@ -844,7 +844,7 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
|
||||
{/* Split body */}
|
||||
<div
|
||||
className={styles.splitBody}
|
||||
className="waterfall-split-body"
|
||||
style={{
|
||||
minHeight: virtualizer.getTotalSize(),
|
||||
height: '100%',
|
||||
@@ -854,11 +854,11 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
fires a load-more via useBoundaryPagination. */}
|
||||
<div
|
||||
ref={loadMoreTopSentinelRef}
|
||||
className={cx(styles.loadMoreSentinel, styles.loadMoreSentinelTop)}
|
||||
className="waterfall-load-more-sentinel waterfall-load-more-sentinel--top"
|
||||
/>
|
||||
<div
|
||||
ref={loadMoreBottomSentinelRef}
|
||||
className={cx(styles.loadMoreSentinel, styles.loadMoreSentinelBottom)}
|
||||
className="waterfall-load-more-sentinel waterfall-load-more-sentinel--bottom"
|
||||
/>
|
||||
<SpanHoverCard
|
||||
hoveredSpanId={hoveredSpanId}
|
||||
@@ -875,9 +875,9 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
minWidth={MIN_SIDEBAR_WIDTH}
|
||||
maxWidth={MAX_SIDEBAR_WIDTH}
|
||||
onResize={setSidebarWidth}
|
||||
className={styles.sidebar}
|
||||
className="waterfall-sidebar"
|
||||
>
|
||||
<table className={styles.treeTable} style={{ width: maxContentWidth }}>
|
||||
<table className="span-tree-table" style={{ width: maxContentWidth }}>
|
||||
<tbody>
|
||||
{virtualItems.map((virtualRow) => {
|
||||
const row = leftRows[virtualRow.index];
|
||||
@@ -887,7 +887,7 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
key={String(virtualRow.key)}
|
||||
data-testid={`cell-0-${span.span_id}`}
|
||||
data-span-id={span.span_id}
|
||||
className={styles.treeRow}
|
||||
className="span-tree-row"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
@@ -900,7 +900,7 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
onMouseLeave={handleRowMouseLeave}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td key={cell.id} className={styles.treeCell}>
|
||||
<td key={cell.id} className="span-tree-cell">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
@@ -912,7 +912,7 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
</ResizableBox>
|
||||
|
||||
{/* Status code column */}
|
||||
<div className={styles.statusCol}>
|
||||
<div className="waterfall-status-col">
|
||||
{virtualItems.map((virtualRow) => {
|
||||
const span = spans[virtualRow.index];
|
||||
const { isSelected, isDimmed, isSelectedNonMatching, isMatching } =
|
||||
@@ -925,10 +925,10 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
key={`status-${String(virtualRow.key)}`}
|
||||
className={cx(styles.statusCell, {
|
||||
[styles.isInterested]: isSelected && (!isFilterActive || isMatching),
|
||||
[styles.isDimmed]: isDimmed,
|
||||
[styles.isSelectedNonMatching]: isSelectedNonMatching,
|
||||
className={cx('status-cell', {
|
||||
'interested-span': isSelected && (!isFilterActive || isMatching),
|
||||
'dimmed-span': isDimmed,
|
||||
'selected-non-matching-span': isSelectedNonMatching,
|
||||
})}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@@ -953,13 +953,13 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
|
||||
{/* Right panel - timeline bars */}
|
||||
<div
|
||||
className={styles.timeline}
|
||||
className="waterfall-timeline"
|
||||
ref={timelineAreaRef}
|
||||
onMouseMove={onCrosshairMove}
|
||||
onMouseLeave={onCrosshairLeave}
|
||||
>
|
||||
{cursorX !== null && (
|
||||
<div className={styles.crosshair} style={{ left: cursorX }} />
|
||||
<div className="waterfall-crosshair" style={{ left: cursorX }} />
|
||||
)}
|
||||
{virtualItems.map((virtualRow) => {
|
||||
const span = spans[virtualRow.index];
|
||||
@@ -968,7 +968,7 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
key={String(virtualRow.key)}
|
||||
data-testid={`cell-1-${span.span_id}`}
|
||||
data-span-id={span.span_id}
|
||||
className={styles.timelineRow}
|
||||
className="timeline-row"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
|
||||
@@ -2,33 +2,18 @@ import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { fireEvent, render, screen } from 'tests/test-utils';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
|
||||
// Local identity-proxy mock for this module so `styles.foo` resolves to
|
||||
// `'foo'` in test assertions. The global `__mocks__/cssMock.ts` stays as
|
||||
// `export default {}`; we override resolution for this specific file only.
|
||||
jest.mock(
|
||||
'../Success.module.scss',
|
||||
() =>
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, prop): string | undefined =>
|
||||
typeof prop === 'string' && prop !== '__esModule' ? prop : undefined,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
import { SpanDuration } from '../Success';
|
||||
import successStyles from '../Success.module.scss';
|
||||
|
||||
const renderWithTraceProvider: typeof render = (ui, options) =>
|
||||
render(ui, options);
|
||||
|
||||
// Constants to avoid string duplication
|
||||
const SPAN_DURATION_TEXT = '1.16 ms';
|
||||
const SPAN_DURATION_CLASS = `.${successStyles.spanDuration}`;
|
||||
const INTERESTED_SPAN_CLASS = successStyles.isInterested;
|
||||
const HIGHLIGHTED_SPAN_CLASS = successStyles.isHighlighted;
|
||||
const DIMMED_SPAN_CLASS = successStyles.isDimmed;
|
||||
const SELECTED_NON_MATCHING_SPAN_CLASS = successStyles.isSelectedNonMatching;
|
||||
const SPAN_DURATION_CLASS = '.span-duration';
|
||||
const INTERESTED_SPAN_CLASS = 'interested-span';
|
||||
const HIGHLIGHTED_SPAN_CLASS = 'highlighted-span';
|
||||
const DIMMED_SPAN_CLASS = 'dimmed-span';
|
||||
const SELECTED_NON_MATCHING_SPAN_CLASS = 'selected-non-matching-span';
|
||||
|
||||
jest.mock('components/TimelineV3/TimelineV3', () => ({
|
||||
__esModule: true,
|
||||
@@ -142,7 +127,10 @@ describe('SpanDuration', () => {
|
||||
|
||||
const spanElement = screen.getByText(SPAN_DURATION_TEXT);
|
||||
|
||||
// Hover over the span should add hovered-span class
|
||||
fireEvent.mouseEnter(spanElement);
|
||||
|
||||
// Mouse leave should remove hovered-span class
|
||||
fireEvent.mouseLeave(spanElement);
|
||||
});
|
||||
|
||||
@@ -271,8 +259,8 @@ describe('SpanDuration', () => {
|
||||
traceMetadata={mockTraceMetadata}
|
||||
selectedSpan={undefined}
|
||||
handleSpanClick={mockSetSelectedSpan}
|
||||
filteredSpanIds={[]}
|
||||
isFilterActive
|
||||
filteredSpanIds={[]} // Empty array but filter is active
|
||||
isFilterActive // This is the key difference
|
||||
/>,
|
||||
);
|
||||
|
||||
|
||||
@@ -2,23 +2,7 @@ import React from 'react';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
|
||||
// Local identity-proxy mock for this module so `styles.foo` resolves to
|
||||
// `'foo'` in test assertions. The global `__mocks__/cssMock.ts` stays as
|
||||
// `export default {}`; we override resolution for this specific file only.
|
||||
jest.mock(
|
||||
'../Success.module.scss',
|
||||
() =>
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, prop): string | undefined =>
|
||||
typeof prop === 'string' && prop !== '__esModule' ? prop : undefined,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
import Success from '../Success';
|
||||
import successStyles from '../Success.module.scss';
|
||||
|
||||
const renderWithTraceProvider: typeof render = (ui, options, customOptions) =>
|
||||
render(ui, options, customOptions);
|
||||
@@ -227,10 +211,10 @@ describe('Span Click User Flows', () => {
|
||||
const FIRST_SPAN_TEST_ID = 'cell-0-span-1';
|
||||
const FIRST_SPAN_DURATION_TEST_ID = 'cell-1-span-1';
|
||||
const SECOND_SPAN_TEST_ID = 'cell-0-span-2';
|
||||
const SPAN_OVERVIEW_CLASS = '.span-overview';
|
||||
const SPAN_DURATION_CLASS = '.span-duration';
|
||||
const INTERESTED_SPAN_CLASS = 'interested-span';
|
||||
const SECOND_SPAN_DURATION_TEST_ID = 'cell-1-span-2';
|
||||
const SPAN_OVERVIEW_CLASS = `.${successStyles.spanOverview}`;
|
||||
const SPAN_DURATION_CLASS = `.${successStyles.spanDuration}`;
|
||||
const INTERESTED_SPAN_CLASS = successStyles.isInterested;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -284,6 +268,7 @@ describe('Span Click User Flows', () => {
|
||||
initialRoute: '/trace',
|
||||
});
|
||||
|
||||
// Wait for initial render and selection
|
||||
await waitFor(() => {
|
||||
const spanDuration = screen.getByTestId(FIRST_SPAN_DURATION_TEST_ID);
|
||||
const spanDurationElement = spanDuration.querySelector(
|
||||
@@ -292,12 +277,14 @@ describe('Span Click User Flows', () => {
|
||||
expect(spanDurationElement).toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
});
|
||||
|
||||
// Click on span-2 to test selection change
|
||||
const span2Duration = screen.getByTestId(SECOND_SPAN_DURATION_TEST_ID);
|
||||
const span2DurationElement = span2Duration.querySelector(
|
||||
SPAN_DURATION_CLASS,
|
||||
) as HTMLElement;
|
||||
await user.click(span2DurationElement);
|
||||
|
||||
// Wait for the state update and re-render
|
||||
await waitFor(() => {
|
||||
const spanDuration = screen.getByTestId(FIRST_SPAN_DURATION_TEST_ID);
|
||||
const spanDurationElement = spanDuration.querySelector(
|
||||
@@ -320,6 +307,7 @@ describe('Span Click User Flows', () => {
|
||||
initialRoute: '/trace',
|
||||
});
|
||||
|
||||
// Wait for initial render and selection
|
||||
await waitFor(() => {
|
||||
const spanOverview = screen.getByTestId(FIRST_SPAN_TEST_ID);
|
||||
const spanDuration = screen.getByTestId(FIRST_SPAN_DURATION_TEST_ID);
|
||||
@@ -330,16 +318,19 @@ describe('Span Click User Flows', () => {
|
||||
SPAN_DURATION_CLASS,
|
||||
) as HTMLElement;
|
||||
|
||||
// Initially both areas should show the same visual selection (first span is auto-selected)
|
||||
expect(spanOverviewElement).toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
expect(spanDurationElement).toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
});
|
||||
|
||||
// Click span-2 to test selection change
|
||||
const span2Overview = screen.getByTestId(SECOND_SPAN_TEST_ID);
|
||||
const span2Element = span2Overview.querySelector(
|
||||
SPAN_OVERVIEW_CLASS,
|
||||
) as HTMLElement;
|
||||
await user.click(span2Element);
|
||||
|
||||
// Wait for the state update and re-render
|
||||
await waitFor(() => {
|
||||
const spanOverview = screen.getByTestId(FIRST_SPAN_TEST_ID);
|
||||
const spanDuration = screen.getByTestId(FIRST_SPAN_DURATION_TEST_ID);
|
||||
@@ -350,15 +341,17 @@ describe('Span Click User Flows', () => {
|
||||
SPAN_DURATION_CLASS,
|
||||
) as HTMLElement;
|
||||
|
||||
// Now span-2 should be selected, span-1 should not
|
||||
expect(spanOverviewElement).not.toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
expect(spanDurationElement).not.toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
|
||||
const span2OverviewSub = screen.getByTestId(SECOND_SPAN_TEST_ID);
|
||||
const span2DurationSub = screen.getByTestId(SECOND_SPAN_DURATION_TEST_ID);
|
||||
const span2OverviewElement = span2OverviewSub.querySelector(
|
||||
// Check that span-2 is selected
|
||||
const span2Overview = screen.getByTestId(SECOND_SPAN_TEST_ID);
|
||||
const span2Duration = screen.getByTestId(SECOND_SPAN_DURATION_TEST_ID);
|
||||
const span2OverviewElement = span2Overview.querySelector(
|
||||
SPAN_OVERVIEW_CLASS,
|
||||
) as HTMLElement;
|
||||
const span2DurationElement = span2DurationSub.querySelector(
|
||||
const span2DurationElement = span2Duration.querySelector(
|
||||
SPAN_DURATION_CLASS,
|
||||
) as HTMLElement;
|
||||
|
||||
@@ -374,6 +367,7 @@ describe('Span Click User Flows', () => {
|
||||
initialRoute: '/trace',
|
||||
});
|
||||
|
||||
// Wait for initial render and selection
|
||||
await waitFor(() => {
|
||||
const span1Overview = screen.getByTestId(FIRST_SPAN_TEST_ID);
|
||||
const span1Element = span1Overview.querySelector(
|
||||
@@ -382,24 +376,27 @@ describe('Span Click User Flows', () => {
|
||||
expect(span1Element).toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
});
|
||||
|
||||
// Click second span
|
||||
const span2Overview = screen.getByTestId(SECOND_SPAN_TEST_ID);
|
||||
const span2Element = span2Overview.querySelector(
|
||||
SPAN_OVERVIEW_CLASS,
|
||||
) as HTMLElement;
|
||||
await user.click(span2Element);
|
||||
|
||||
// Wait for the state update and re-render
|
||||
await waitFor(() => {
|
||||
const span1Overview = screen.getByTestId(FIRST_SPAN_TEST_ID);
|
||||
const span1Element = span1Overview.querySelector(
|
||||
SPAN_OVERVIEW_CLASS,
|
||||
) as HTMLElement;
|
||||
const span2OverviewSub = screen.getByTestId(SECOND_SPAN_TEST_ID);
|
||||
const span2ElementSub = span2OverviewSub.querySelector(
|
||||
const span2Overview = screen.getByTestId(SECOND_SPAN_TEST_ID);
|
||||
const span2Element = span2Overview.querySelector(
|
||||
SPAN_OVERVIEW_CLASS,
|
||||
) as HTMLElement;
|
||||
|
||||
// Second span should be selected, first should not
|
||||
expect(span1Element).not.toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
expect(span2ElementSub).toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
expect(span2Element).toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -429,6 +426,7 @@ describe('Span Click User Flows', () => {
|
||||
{ initialRoute: '/trace' },
|
||||
);
|
||||
|
||||
// Click on the actual span element (not the wrapper)
|
||||
const spanOverview = screen.getByTestId(FIRST_SPAN_TEST_ID);
|
||||
const spanElement = spanOverview.querySelector(
|
||||
SPAN_OVERVIEW_CLASS,
|
||||
@@ -440,6 +438,7 @@ describe('Span Click User Flows', () => {
|
||||
expect(mockUrlQuery.get('anotherParam')).toBe('anotherValue');
|
||||
expect(mockUrlQuery.get('spanId')).toBe('span-1');
|
||||
|
||||
// Verify navigation was called with all parameters
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith({
|
||||
search: expect.stringMatching(
|
||||
/existingParam=existingValue.*anotherParam=anotherValue.*spanId=span-1/,
|
||||
|
||||
@@ -15,13 +15,10 @@ import {
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import cx from 'classnames';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { GripVertical } from '@signozhq/icons';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import styles from './FieldsSettings.module.scss';
|
||||
|
||||
function SortableField({
|
||||
field,
|
||||
onRemove,
|
||||
@@ -43,17 +40,14 @@ function SortableField({
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cx(
|
||||
styles.fieldItem,
|
||||
allowDrag ? styles.isDragEnabled : styles.isDragDisabled,
|
||||
)}
|
||||
className={`fs-field-item ${allowDrag ? 'drag-enabled' : 'drag-disabled'}`}
|
||||
>
|
||||
<div {...attributes} {...listeners} className={styles.dragHandle}>
|
||||
<div {...attributes} {...listeners} className="drag-handle">
|
||||
{allowDrag && <GripVertical size={14} />}
|
||||
<span className={styles.fieldKey}>{field.key}</span>
|
||||
<span className="fs-field-key">{field.key}</span>
|
||||
</div>
|
||||
<Button
|
||||
className={cx(styles.removeBtn, 'periscope-btn')}
|
||||
className="remove-field-btn periscope-btn"
|
||||
variant="outlined"
|
||||
color="destructive"
|
||||
size="sm"
|
||||
@@ -100,9 +94,9 @@ function AddedFields({
|
||||
const allowDrag = inputValue.length === 0;
|
||||
|
||||
return (
|
||||
<div className={cx(styles.section, styles.sectionAdded)}>
|
||||
<div className={styles.sectionHeader}>ADDED FIELDS</div>
|
||||
<div className={styles.addedList}>
|
||||
<div className="fs-section fs-added">
|
||||
<div className="fs-section-header">ADDED FIELDS</div>
|
||||
<div className="fs-added-list">
|
||||
<OverlayScrollbar>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
@@ -110,7 +104,7 @@ function AddedFields({
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{filteredFields.length === 0 ? (
|
||||
<div className={styles.noValues}>No values found</div>
|
||||
<div className="fs-no-values">No values found</div>
|
||||
) : (
|
||||
<SortableContext
|
||||
items={fields.map((f) => f.key)}
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
cursor: pointer;
|
||||
color: var(--l2-foreground);
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
background-color: var(--l1-background);
|
||||
height: 40px;
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sectionAdded {
|
||||
max-height: 40%;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.sectionOther {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.addedList {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.otherList {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
// Ant Skeleton.Input rendered inside the loading state — override its
|
||||
// hard-coded width.
|
||||
:global(.ant-skeleton-input) {
|
||||
width: 300px;
|
||||
margin: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.noValues {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.limitHint {
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.fieldItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
font-size: 13px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--l2-background);
|
||||
|
||||
.removeBtn,
|
||||
.addBtn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dragHandle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.fieldKey {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.isDragEnabled {
|
||||
cursor: grab;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.isDragDisabled {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.otherFieldItem {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.removeBtn,
|
||||
.addBtn {
|
||||
padding: 4px 10px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
justify-content: space-between;
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
.fields-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
overflow: hidden;
|
||||
|
||||
.fs-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.fs-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fs-close-icon {
|
||||
cursor: pointer;
|
||||
color: var(--l2-foreground);
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fs-search {
|
||||
.fs-search-input {
|
||||
background-color: var(--l1-background);
|
||||
height: 40px;
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fs-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.fs-added {
|
||||
max-height: 40%;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
&.fs-other {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fs-section-header {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.fs-added-list {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fs-other-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-skeleton-input {
|
||||
width: 300px;
|
||||
margin: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.fs-no-values {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.fs-limit-hint {
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.fs-field-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
font-size: 13px;
|
||||
|
||||
.drag-handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.fs-field-key {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.drag-enabled {
|
||||
cursor: grab;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
&.drag-disabled {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
&.other-field-item {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.remove-field-btn,
|
||||
.add-field-btn {
|
||||
padding: 4px 10px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--l2-background);
|
||||
|
||||
.remove-field-btn,
|
||||
.add-field-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fs-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
justify-content: space-between;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import AddedFields from './AddedFields';
|
||||
import OtherFields from './OtherFields';
|
||||
|
||||
import styles from './FieldsSettings.module.scss';
|
||||
import './FieldsSettings.styles.scss';
|
||||
|
||||
const MAX_FIELDS_DEFAULT = 10;
|
||||
|
||||
@@ -89,18 +89,18 @@ function FieldsSettings({
|
||||
const isAtLimit = draftFields.length >= maxFields;
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>
|
||||
<div className="fields-settings">
|
||||
<div className="fs-header">
|
||||
<div className="fs-title">
|
||||
<TableColumnsSplit size={16} />
|
||||
{title}
|
||||
</div>
|
||||
<X className={styles.closeIcon} size={16} onClick={onClose} />
|
||||
<X className="fs-close-icon" size={16} onClick={onClose} />
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<section className="fs-search">
|
||||
<Input
|
||||
className={styles.searchInput}
|
||||
className="fs-search-input"
|
||||
type="text"
|
||||
value={inputValue}
|
||||
placeholder="Search for a field..."
|
||||
@@ -123,7 +123,7 @@ function FieldsSettings({
|
||||
/>
|
||||
|
||||
{hasUnsavedChanges && (
|
||||
<div className={styles.footer}>
|
||||
<div className="fs-footer">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Skeleton } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import styles from './FieldsSettings.module.scss';
|
||||
|
||||
interface OtherFieldsProps {
|
||||
dataSource: DataSource;
|
||||
debouncedInputValue: string;
|
||||
@@ -53,9 +50,9 @@ function OtherFields({
|
||||
|
||||
if (isFetching) {
|
||||
return (
|
||||
<div className={cx(styles.section, styles.sectionOther)}>
|
||||
<div className={styles.sectionHeader}>OTHER FIELDS</div>
|
||||
<div className={styles.otherList}>
|
||||
<div className="fs-section fs-other">
|
||||
<div className="fs-section-header">OTHER FIELDS</div>
|
||||
<div className="fs-other-list">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Skeleton.Input active size="small" key={i} />
|
||||
@@ -66,23 +63,20 @@ function OtherFields({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(styles.section, styles.sectionOther)}>
|
||||
<div className={styles.sectionHeader}>OTHER FIELDS</div>
|
||||
<div className={styles.otherList}>
|
||||
<div className="fs-section fs-other">
|
||||
<div className="fs-section-header">OTHER FIELDS</div>
|
||||
<div className="fs-other-list">
|
||||
<OverlayScrollbar>
|
||||
<>
|
||||
{otherFields.length === 0 ? (
|
||||
<div className={styles.noValues}>No values found</div>
|
||||
<div className="fs-no-values">No values found</div>
|
||||
) : (
|
||||
otherFields.map((attr) => (
|
||||
<div
|
||||
key={attr.key}
|
||||
className={cx(styles.fieldItem, styles.otherFieldItem)}
|
||||
>
|
||||
<span className={styles.fieldKey}>{attr.key}</span>
|
||||
<div key={attr.key} className="fs-field-item other-field-item">
|
||||
<span className="fs-field-key">{attr.key}</span>
|
||||
{!isAtLimit && (
|
||||
<Button
|
||||
className={cx(styles.addBtn, 'periscope-btn')}
|
||||
className="add-field-btn periscope-btn"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
@@ -94,7 +88,7 @@ function OtherFields({
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{isAtLimit && <div className={styles.limitHint}>Maximum 10 fields</div>}
|
||||
{isAtLimit && <div className="fs-limit-hint">Maximum 10 fields</div>}
|
||||
</>
|
||||
</OverlayScrollbar>
|
||||
</div>
|
||||
|
||||
@@ -32,9 +32,7 @@ import TraceWaterfall from './TraceWaterfall/TraceWaterfall';
|
||||
import { IInterestedSpan } from './TraceWaterfall/types';
|
||||
import { getAncestorSpanIds } from './TraceWaterfall/utils';
|
||||
|
||||
import cx from 'classnames';
|
||||
|
||||
import styles from './TraceDetailsV3.module.scss';
|
||||
import './TraceDetailsV3.styles.scss';
|
||||
|
||||
function TraceDetailsV3(): JSX.Element {
|
||||
const { id: traceId } = useParams<TraceDetailV3URLProps>();
|
||||
@@ -287,7 +285,7 @@ function TraceDetailsV3(): JSX.Element {
|
||||
|
||||
return (
|
||||
<TraceStoreSync aggregations={traceData?.payload?.aggregations}>
|
||||
<div className={styles.root}>
|
||||
<div className="trace-details-v3">
|
||||
<TraceDetailsHeader
|
||||
filterMetadata={filterMetadata}
|
||||
onFilteredSpansChange={handleFilteredSpansChange}
|
||||
@@ -299,20 +297,20 @@ function TraceDetailsV3(): JSX.Element {
|
||||
<NoData />
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.content}>
|
||||
<div className="trace-details-v3__content">
|
||||
<Collapse
|
||||
// @ts-expect-error motion is passed through to rc-collapse to disable animation
|
||||
motion={false}
|
||||
activeKey={activeKeys.filter((k) => k === 'flame')}
|
||||
onChange={(): void => handleCollapseChange('flame')}
|
||||
size="small"
|
||||
className={styles.flameCollapse}
|
||||
className="trace-details-v3__flame-collapse"
|
||||
items={[
|
||||
{
|
||||
key: 'flame',
|
||||
label: (
|
||||
<div className={styles.collapseLabel}>
|
||||
<span className={styles.collapseTitle}>
|
||||
<div className="trace-details-v3__collapse-label">
|
||||
<span className="trace-details-v3__collapse-title">
|
||||
Flame Graph
|
||||
{traceData?.payload?.totalSpansCount &&
|
||||
traceData.payload.totalSpansCount > FLAMEGRAPH_SPAN_LIMIT && (
|
||||
@@ -323,15 +321,17 @@ function TraceDetailsV3(): JSX.Element {
|
||||
)}
|
||||
</span>
|
||||
{traceData?.payload?.totalSpansCount ? (
|
||||
<span className={styles.collapseCount}>
|
||||
<span className={styles.collapseCountItem}>
|
||||
<span className="trace-details-v3__collapse-count">
|
||||
<span className="trace-details-v3__collapse-count-item">
|
||||
<ChartNoAxesGantt size={13} />
|
||||
Spans: {traceData.payload.totalSpansCount}
|
||||
</span>
|
||||
<span
|
||||
className={cx(styles.collapseCountItem, {
|
||||
[styles.hasErrors]: traceData.payload.totalErrorSpansCount > 0,
|
||||
})}
|
||||
className={`trace-details-v3__collapse-count-item${
|
||||
traceData.payload.totalErrorSpansCount > 0
|
||||
? ' trace-details-v3__collapse-count-errors'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<TriangleAlert size={13} />
|
||||
Errors: {traceData.payload.totalErrorSpansCount ?? 0}
|
||||
@@ -360,9 +360,11 @@ function TraceDetailsV3(): JSX.Element {
|
||||
activeKey={activeKeys.filter((k) => k === 'waterfall')}
|
||||
onChange={(): void => handleCollapseChange('waterfall')}
|
||||
size="small"
|
||||
className={cx(styles.waterfallCollapse, {
|
||||
[styles.isDocked]: isWaterfallDocked,
|
||||
})}
|
||||
className={`trace-details-v3__waterfall-collapse${
|
||||
isWaterfallDocked
|
||||
? ' trace-details-v3__waterfall-collapse--docked'
|
||||
: ''
|
||||
}`}
|
||||
items={[
|
||||
{
|
||||
key: 'waterfall',
|
||||
@@ -373,7 +375,7 @@ function TraceDetailsV3(): JSX.Element {
|
||||
/>
|
||||
|
||||
{panelState.isOpen && isDocked && (
|
||||
<div className={styles.dockedSpanDetails}>
|
||||
<div className="trace-details-v3__docked-span-details">
|
||||
<SpanDetailsPanel
|
||||
panelState={panelState}
|
||||
selectedSpan={selectedSpan}
|
||||
|
||||
@@ -162,24 +162,5 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/infra_monitoring/jobs", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListJobs),
|
||||
handler.OpenAPIDef{
|
||||
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.",
|
||||
Request: new(inframonitoringtypes.PostableJobs),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(inframonitoringtypes.Jobs),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,14 +7,11 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addRoleRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v1/roles", handler.New(provider.authzMiddleware.Check(provider.authzHandler.Create, authtypes.Relation{Verb: coretypes.VerbCreate}, coretypes.ResourceRole, roleCollectionSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v1/roles", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Create), handler.OpenAPIDef{
|
||||
ID: "CreateRole",
|
||||
Tags: []string{"role"},
|
||||
Summary: "Create role",
|
||||
@@ -26,14 +23,12 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbCreate)}),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/roles", handler.New(provider.authzMiddleware.Check(provider.authzHandler.List, authtypes.Relation{Verb: coretypes.VerbList}, coretypes.ResourceRole, roleCollectionSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v1/roles", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.List), handler.OpenAPIDef{
|
||||
ID: "ListRoles",
|
||||
Tags: []string{"role"},
|
||||
Summary: "List roles",
|
||||
@@ -45,14 +40,12 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbList)}),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.Check(provider.authzHandler.Get, authtypes.Relation{Verb: coretypes.VerbRead}, coretypes.ResourceRole, provider.roleInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Get), handler.OpenAPIDef{
|
||||
ID: "GetRole",
|
||||
Tags: []string{"role"},
|
||||
Summary: "Get role",
|
||||
@@ -64,14 +57,12 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbRead)}),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(provider.authzMiddleware.Check(provider.authzHandler.GetObjects, authtypes.Relation{Verb: coretypes.VerbRead}, coretypes.ResourceRole, provider.roleInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.GetObjects), handler.OpenAPIDef{
|
||||
ID: "GetObjects",
|
||||
Tags: []string{"role"},
|
||||
Summary: "Get objects for a role by relation",
|
||||
@@ -83,14 +74,12 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbRead)}),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.Check(provider.authzHandler.Patch, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceRole, provider.roleInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Patch), handler.OpenAPIDef{
|
||||
ID: "PatchRole",
|
||||
Tags: []string{"role"},
|
||||
Summary: "Patch role",
|
||||
@@ -102,14 +91,12 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbUpdate)}),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(provider.authzMiddleware.Check(provider.authzHandler.PatchObjects, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceRole, provider.roleInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.PatchObjects), handler.OpenAPIDef{
|
||||
ID: "PatchObjects",
|
||||
Tags: []string{"role"},
|
||||
Summary: "Patch objects for a role by relation",
|
||||
@@ -121,14 +108,12 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbUpdate)}),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.Check(provider.authzHandler.Delete, authtypes.Relation{Verb: coretypes.VerbDelete}, coretypes.ResourceRole, provider.roleInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Delete), handler.OpenAPIDef{
|
||||
ID: "DeleteRole",
|
||||
Tags: []string{"role"},
|
||||
Summary: "Delete role",
|
||||
@@ -140,33 +125,10 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbDelete)}),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func roleCollectionSelectorCallback(_ *http.Request, _ authtypes.Claims) ([]coretypes.Selector, error) {
|
||||
return []coretypes.Selector{
|
||||
coretypes.TypeRole.MustSelector(coretypes.WildCardSelectorString),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) roleInstanceSelectorCallback(req *http.Request, claims authtypes.Claims) ([]coretypes.Selector, error) {
|
||||
roleID, err := valuer.NewUUID(mux.Vars(req)["id"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
role, err := provider.authzService.Get(req.Context(), valuer.MustNewUUID(claims.OrgID), roleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []coretypes.Selector{
|
||||
coretypes.TypeRole.MustSelector(role.Name),
|
||||
coretypes.TypeRole.MustSelector(coretypes.WildCardSelectorString),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.Create, authtypes.Relation{Verb: coretypes.VerbCreate}, coretypes.ResourceServiceAccount, serviceAccountCollectionSelectorCallback, []string{
|
||||
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.Create, authtypes.Relation{Verb: coretypes.VerbCreate}, coretypes.ResourceMetaResourcesServiceAccount, serviceAccountCollectionSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
ID: "CreateServiceAccount",
|
||||
@@ -31,12 +31,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbCreate)}),
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourcesServiceAccount.Scope(coretypes.VerbCreate)}),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.List, authtypes.Relation{Verb: coretypes.VerbList}, coretypes.ResourceServiceAccount, serviceAccountCollectionSelectorCallback, []string{
|
||||
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.List, authtypes.Relation{Verb: coretypes.VerbList}, coretypes.ResourceMetaResourcesServiceAccount, serviceAccountCollectionSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
ID: "ListServiceAccounts",
|
||||
@@ -50,7 +50,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbList)}),
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourcesServiceAccount.Scope(coretypes.VerbList)}),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -135,10 +135,10 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/roles/{rid}", handler.New(provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.DeleteRole, []middleware.AuthZCheckGroup{
|
||||
{{Relation: authtypes.Relation{Verb: coretypes.VerbDetach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
|
||||
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}}},
|
||||
{{Relation: authtypes.Relation{Verb: coretypes.VerbDetach}, Resource: coretypes.ResourceRole, SelectorCallback: provider.roleDetachSelectorFromPath, Roles: []string{
|
||||
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceRole, SelectorCallback: provider.roleAttachSelectorFromPath, Roles: []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}}},
|
||||
}), handler.OpenAPIDef{
|
||||
@@ -153,7 +153,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbDetach), coretypes.ResourceRole.Scope(coretypes.VerbDetach)}),
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach), coretypes.ResourceRole.Scope(coretypes.VerbAttach)}),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -213,13 +213,8 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.CreateFactorAPIKey, []middleware.AuthZCheckGroup{
|
||||
{{Relation: authtypes.Relation{Verb: coretypes.VerbCreate}, Resource: coretypes.ResourceMetaResourceFactorAPIKey, SelectorCallback: factorAPIKeyCollectionSelectorCallback, Roles: []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}}},
|
||||
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}}},
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.CreateFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
ID: "CreateServiceAccountKey",
|
||||
Tags: []string{"serviceaccount"},
|
||||
@@ -232,12 +227,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceFactorAPIKey.Scope(coretypes.VerbCreate), coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach)}),
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.ListFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbList}, coretypes.ResourceMetaResourceFactorAPIKey, factorAPIKeyCollectionSelectorCallback, []string{
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.ListFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbRead}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
ID: "ListServiceAccountKeys",
|
||||
@@ -251,12 +246,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceFactorAPIKey.Scope(coretypes.VerbList)}),
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbRead)}),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.UpdateFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceMetaResourceFactorAPIKey, factorAPIKeyInstanceSelectorCallback, []string{
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.UpdateFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
ID: "UpdateServiceAccountKey",
|
||||
@@ -270,18 +265,13 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceFactorAPIKey.Scope(coretypes.VerbUpdate)}),
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.RevokeFactorAPIKey, []middleware.AuthZCheckGroup{
|
||||
{{Relation: authtypes.Relation{Verb: coretypes.VerbDelete}, Resource: coretypes.ResourceMetaResourceFactorAPIKey, SelectorCallback: factorAPIKeyInstanceSelectorCallback, Roles: []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}}},
|
||||
{{Relation: authtypes.Relation{Verb: coretypes.VerbDetach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}}},
|
||||
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.RevokeFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
|
||||
authtypes.SigNozAdminRoleName,
|
||||
}), handler.OpenAPIDef{
|
||||
ID: "RevokeServiceAccountKey",
|
||||
Tags: []string{"serviceaccount"},
|
||||
@@ -294,7 +284,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceFactorAPIKey.Scope(coretypes.VerbDelete), coretypes.ResourceServiceAccount.Scope(coretypes.VerbDetach)}),
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -302,7 +292,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) roleDetachSelectorFromPath(req *http.Request, claims authtypes.Claims) ([]coretypes.Selector, error) {
|
||||
func (provider *provider) roleAttachSelectorFromPath(req *http.Request, claims authtypes.Claims) ([]coretypes.Selector, error) {
|
||||
roleID, err := valuer.NewUUID(mux.Vars(req)["rid"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -343,28 +333,9 @@ func (provider *provider) roleAttachSelectorFromBody(req *http.Request, claims a
|
||||
}, nil
|
||||
}
|
||||
|
||||
func factorAPIKeyCollectionSelectorCallback(_ *http.Request, _ authtypes.Claims) ([]coretypes.Selector, error) {
|
||||
return []coretypes.Selector{
|
||||
coretypes.TypeMetaResource.MustSelector(coretypes.WildCardSelectorString),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func factorAPIKeyInstanceSelectorCallback(req *http.Request, _ authtypes.Claims) ([]coretypes.Selector, error) {
|
||||
fid := mux.Vars(req)["fid"]
|
||||
fidSelector, err := coretypes.TypeMetaResource.Selector(fid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []coretypes.Selector{
|
||||
fidSelector,
|
||||
coretypes.TypeMetaResource.MustSelector(coretypes.WildCardSelectorString),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func serviceAccountCollectionSelectorCallback(_ *http.Request, _ authtypes.Claims) ([]coretypes.Selector, error) {
|
||||
return []coretypes.Selector{
|
||||
coretypes.TypeServiceAccount.MustSelector(coretypes.WildCardSelectorString),
|
||||
coretypes.TypeMetaResources.MustSelector(coretypes.WildCardSelectorString),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ func (handler *handler) Patch(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
render.Success(rw, http.StatusAccepted, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) PatchObjects(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -213,27 +213,3 @@ func (h *handler) ListStatefulSets(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
render.Success(rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *handler) ListJobs(rw http.ResponseWriter, req *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(req.Context())
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
var parsedReq inframonitoringtypes.PostableJobs
|
||||
if err := binding.JSON.BindBody(req.Body, &parsedReq); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.module.ListJobs(req.Context(), orgID, &parsedReq)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// buildJobRecords assembles the page records. Pod phase counts come from
|
||||
// phaseCounts in both modes; every row is a group of pods (one job in
|
||||
// list mode, an arbitrary roll-up in grouped_list mode), so there's no
|
||||
// per-row "current phase" concept.
|
||||
func buildJobRecords(
|
||||
resp *qbtypes.QueryRangeResponse,
|
||||
pageGroups []map[string]string,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
metadataMap map[string]map[string]string,
|
||||
phaseCounts map[string]podPhaseCounts,
|
||||
) []inframonitoringtypes.JobRecord {
|
||||
metricsMap := parseFullQueryResponse(resp, groupBy)
|
||||
|
||||
records := make([]inframonitoringtypes.JobRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
jobName := labels[jobNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.JobRecord{ // initialize with default values
|
||||
JobName: jobName,
|
||||
JobCPU: -1,
|
||||
JobCPURequest: -1,
|
||||
JobCPULimit: -1,
|
||||
JobMemory: -1,
|
||||
JobMemoryRequest: -1,
|
||||
JobMemoryLimit: -1,
|
||||
DesiredSuccessfulPods: -1,
|
||||
ActivePods: -1,
|
||||
FailedPods: -1,
|
||||
SuccessfulPods: -1,
|
||||
Meta: map[string]string{},
|
||||
}
|
||||
|
||||
if metrics, ok := metricsMap[compositeKey]; ok {
|
||||
if v, exists := metrics["A"]; exists {
|
||||
record.JobCPU = v
|
||||
}
|
||||
if v, exists := metrics["B"]; exists {
|
||||
record.JobCPURequest = v
|
||||
}
|
||||
if v, exists := metrics["C"]; exists {
|
||||
record.JobCPULimit = v
|
||||
}
|
||||
if v, exists := metrics["D"]; exists {
|
||||
record.JobMemory = v
|
||||
}
|
||||
if v, exists := metrics["E"]; exists {
|
||||
record.JobMemoryRequest = v
|
||||
}
|
||||
if v, exists := metrics["F"]; exists {
|
||||
record.JobMemoryLimit = v
|
||||
}
|
||||
if v, exists := metrics["H"]; exists {
|
||||
record.DesiredSuccessfulPods = int(v)
|
||||
}
|
||||
if v, exists := metrics["I"]; exists {
|
||||
record.ActivePods = int(v)
|
||||
}
|
||||
if v, exists := metrics["J"]; exists {
|
||||
record.FailedPods = int(v)
|
||||
}
|
||||
if v, exists := metrics["K"]; exists {
|
||||
record.SuccessfulPods = int(v)
|
||||
}
|
||||
}
|
||||
|
||||
if phaseCountsForGroup, ok := phaseCounts[compositeKey]; ok {
|
||||
record.PodCountsByPhase = inframonitoringtypes.PodCountsByPhase{
|
||||
Pending: phaseCountsForGroup.Pending,
|
||||
Running: phaseCountsForGroup.Running,
|
||||
Succeeded: phaseCountsForGroup.Succeeded,
|
||||
Failed: phaseCountsForGroup.Failed,
|
||||
Unknown: phaseCountsForGroup.Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
if attrs, ok := metadataMap[compositeKey]; ok {
|
||||
for k, v := range attrs {
|
||||
record.Meta[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
func (m *module) getTopJobGroups(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *inframonitoringtypes.PostableJobs,
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
queryNamesForOrderBy := orderByToJobsQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
topReq := &qbtypes.QueryRangeRequest{
|
||||
Start: uint64(req.Start),
|
||||
End: uint64(req.End),
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{
|
||||
Queries: make([]qbtypes.QueryEnvelope, 0, len(queryNamesForOrderBy)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, envelope := range m.newJobsTableListQuery().CompositeQuery.Queries {
|
||||
if !slices.Contains(queryNamesForOrderBy, envelope.GetQueryName()) {
|
||||
continue
|
||||
}
|
||||
copied := envelope
|
||||
if copied.Type == qbtypes.QueryTypeBuilder {
|
||||
existingExpr := ""
|
||||
if f := copied.GetFilter(); f != nil {
|
||||
existingExpr = f.Expression
|
||||
}
|
||||
reqFilterExpr := ""
|
||||
if req.Filter != nil {
|
||||
reqFilterExpr = req.Filter.Expression
|
||||
}
|
||||
merged := mergeFilterExpressions(existingExpr, reqFilterExpr)
|
||||
copied.SetFilter(&qbtypes.Filter{Expression: merged})
|
||||
copied.SetGroupBy(req.GroupBy)
|
||||
}
|
||||
topReq.CompositeQuery.Queries = append(topReq.CompositeQuery.Queries, copied)
|
||||
}
|
||||
|
||||
resp, err := m.querier.QueryRange(ctx, orgID, topReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
|
||||
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
|
||||
}
|
||||
|
||||
func (m *module) getJobsTableMetadata(ctx context.Context, req *inframonitoringtypes.PostableJobs) (map[string]map[string]string, error) {
|
||||
var nonGroupByAttrs []string
|
||||
for _, key := range jobAttrKeysForMetadata {
|
||||
if !isKeyInGroupByAttrs(req.GroupBy, key) {
|
||||
nonGroupByAttrs = append(nonGroupByAttrs, key)
|
||||
}
|
||||
}
|
||||
return m.getMetadata(ctx, jobsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
jobNameAttrKey = "k8s.job.name"
|
||||
jobsBaseFilterExpr = "k8s.job.name != ''"
|
||||
)
|
||||
|
||||
var jobNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: jobNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
}
|
||||
|
||||
// jobsTableMetricNamesList drives the existence/retention check.
|
||||
// Includes k8s.pod.phase even though phase isn't part of the QB composite query —
|
||||
// it is queried separately via getPerGroupPodPhaseCounts, and we want the
|
||||
// response to short-circuit cleanly when the phase metric is absent.
|
||||
var jobsTableMetricNamesList = []string{
|
||||
"k8s.pod.phase",
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory.working_set",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
"k8s.job.active_pods",
|
||||
"k8s.job.failed_pods",
|
||||
"k8s.job.successful_pods",
|
||||
"k8s.job.desired_successful_pods",
|
||||
}
|
||||
|
||||
// Carried forward from v1 jobAttrsToEnrich
|
||||
// (pkg/query-service/app/inframetrics/jobs.go:31-35).
|
||||
var jobAttrKeysForMetadata = []string{
|
||||
"k8s.job.name",
|
||||
"k8s.namespace.name",
|
||||
"k8s.cluster.name",
|
||||
}
|
||||
|
||||
// orderByToJobsQueryNames maps the orderBy column to the query name
|
||||
// used for ranking job groups. v2 B/C/E/F are direct metrics, no
|
||||
// formula deps — so unlike v1 we don't carry A/D.
|
||||
var orderByToJobsQueryNames = map[string][]string{
|
||||
inframonitoringtypes.JobsOrderByCPU: {"A"},
|
||||
inframonitoringtypes.JobsOrderByCPURequest: {"B"},
|
||||
inframonitoringtypes.JobsOrderByCPULimit: {"C"},
|
||||
inframonitoringtypes.JobsOrderByMemory: {"D"},
|
||||
inframonitoringtypes.JobsOrderByMemoryRequest: {"E"},
|
||||
inframonitoringtypes.JobsOrderByMemoryLimit: {"F"},
|
||||
inframonitoringtypes.JobsOrderByDesiredSuccessfulPods: {"H"},
|
||||
inframonitoringtypes.JobsOrderByActivePods: {"I"},
|
||||
inframonitoringtypes.JobsOrderByFailedPods: {"J"},
|
||||
inframonitoringtypes.JobsOrderBySuccessfulPods: {"K"},
|
||||
}
|
||||
|
||||
// newJobsTableListQuery builds the composite QB v5 request for the jobs list.
|
||||
// Ten builder queries: A..F roll up pod-level metrics by job, H/I/J/K take the
|
||||
// latest job-level desired/active/failed/successful counts. Restarts (v1 query G)
|
||||
// is intentionally omitted to match the v2 pods/deployments pattern.
|
||||
//
|
||||
// Every builder query carries the base filter `jobsBaseFilterExpr`. Reason:
|
||||
// pod-level metrics (A..F) are emitted for every pod regardless of whether the
|
||||
// pod belongs to a Job; only Job-owned pods carry the `k8s.job.name` resource
|
||||
// attribute. Without this filter, standalone pods and pods owned by other
|
||||
// workloads (Deployment/StatefulSet/DaemonSet/...) collapse into a single
|
||||
// empty-string group under the default groupBy.
|
||||
func (m *module) newJobsTableListQuery() *qbtypes.QueryRangeRequest {
|
||||
queries := []qbtypes.QueryEnvelope{
|
||||
// Query A: k8s.pod.cpu.usage — sum of pod CPU within the group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.cpu.usage",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
|
||||
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query B: k8s.pod.cpu_request_utilization — avg across pods in the group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "B",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.cpu_request_utilization",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationAvg,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
|
||||
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query C: k8s.pod.cpu_limit_utilization — avg across pods in the group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "C",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.cpu_limit_utilization",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationAvg,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
|
||||
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query D: k8s.pod.memory.working_set — sum of pod memory within the group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "D",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.memory.working_set",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
|
||||
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query E: k8s.pod.memory_request_utilization — avg across pods in the group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "E",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.memory_request_utilization",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationAvg,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
|
||||
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query F: k8s.pod.memory_limit_utilization — avg across pods in the group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "F",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.pod.memory_limit_utilization",
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationAvg,
|
||||
ReduceTo: qbtypes.ReduceToAvg,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
|
||||
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query H: k8s.job.desired_successful_pods — latest known desired completion count per group.
|
||||
// v1 used TimeAggregationAnyLast (v3) → mapped to TimeAggregationLatest in v5;
|
||||
// SpaceAggregationSum + ReduceToLast preserve v1's "latest, summed across the group".
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "H",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.job.desired_successful_pods",
|
||||
TimeAggregation: metrictypes.TimeAggregationLatest,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToLast,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
|
||||
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query I: k8s.job.active_pods — latest known active pod count per group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "I",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.job.active_pods",
|
||||
TimeAggregation: metrictypes.TimeAggregationLatest,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToLast,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
|
||||
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query J: k8s.job.failed_pods — cumulative failed pod count per group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "J",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.job.failed_pods",
|
||||
TimeAggregation: metrictypes.TimeAggregationLatest,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToLast,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
|
||||
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
// Query K: k8s.job.successful_pods — cumulative successful pod count per group.
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "K",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "k8s.job.successful_pods",
|
||||
TimeAggregation: metrictypes.TimeAggregationLatest,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
ReduceTo: qbtypes.ReduceToLast,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
|
||||
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return &qbtypes.QueryRangeRequest{
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{
|
||||
Queries: queries,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -803,100 +803,3 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableJobs) (*inframonitoringtypes.Jobs, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &inframonitoringtypes.Jobs{}
|
||||
|
||||
if req.OrderBy == nil {
|
||||
req.OrderBy = &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: inframonitoringtypes.JobsOrderByCPU,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.GroupBy) == 0 {
|
||||
req.GroupBy = []qbtypes.GroupByKey{jobNameGroupByKey}
|
||||
resp.Type = inframonitoringtypes.ResponseTypeList
|
||||
} else {
|
||||
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
|
||||
}
|
||||
|
||||
// Bake the jobs base filter into req.Filter so all downstream helpers pick it up.
|
||||
if req.Filter == nil {
|
||||
req.Filter = &qbtypes.Filter{}
|
||||
}
|
||||
req.Filter.Expression = mergeFilterExpressions(jobsBaseFilterExpr, req.Filter.Expression)
|
||||
|
||||
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Total = len(metadataMap)
|
||||
|
||||
pageGroups, err := m.getTopJobGroups(ctx, orgID, req, metadataMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pageGroups) == 0 {
|
||||
resp.Records = []inframonitoringtypes.JobRecord{}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
filterExpr := ""
|
||||
if req.Filter != nil {
|
||||
filterExpr = req.Filter.Expression
|
||||
}
|
||||
|
||||
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newJobsTableListQuery())
|
||||
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reuse the pods phase-counts CTE function via a temp struct — it reads only
|
||||
// Start/End/Filter/GroupBy from PostablePods. Pods owned by a Job carry
|
||||
// k8s.job.name as a resource attribute, so default-groupBy gives
|
||||
// per-job phase counts automatically.
|
||||
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
|
||||
Start: req.Start,
|
||||
End: req.End,
|
||||
Filter: req.Filter,
|
||||
GroupBy: req.GroupBy,
|
||||
}, pageGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Records = buildJobRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
|
||||
resp.Warning = queryResp.Warning
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ type Handler interface {
|
||||
ListVolumes(http.ResponseWriter, *http.Request)
|
||||
ListDeployments(http.ResponseWriter, *http.Request)
|
||||
ListStatefulSets(http.ResponseWriter, *http.Request)
|
||||
ListJobs(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type Module interface {
|
||||
@@ -29,5 +28,4 @@ type Module interface {
|
||||
ListVolumes(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableVolumes) (*inframonitoringtypes.Volumes, error)
|
||||
ListDeployments(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableDeployments) (*inframonitoringtypes.Deployments, error)
|
||||
ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableStatefulSets) (*inframonitoringtypes.StatefulSets, error)
|
||||
ListJobs(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableJobs) (*inframonitoringtypes.Jobs, error)
|
||||
}
|
||||
|
||||
@@ -200,7 +200,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddServiceAccountManagedRoleTransactionsFactory(sqlstore),
|
||||
sqlmigration.NewAddSpanMapperFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddLLMPricingRulesFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewMigrateMetaresourcesTuplesFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type migrateMetaresourcesTuples struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewMigrateMetaresourcesTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("migrate_metaresources_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &migrateMetaresourcesTuples{sqlstore: sqlstore}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *migrateMetaresourcesTuples) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
// migrationTuple describes a single FGA tuple to insert.
|
||||
type migrationTuple struct {
|
||||
roleName string // "signoz-admin", "signoz-editor", "signoz-viewer"
|
||||
objectType string // "serviceaccount", "user", "role", "metaresource"
|
||||
objectName string // "serviceaccount", "user", "role", etc.
|
||||
relation string // "create", "list", "detach", etc.
|
||||
}
|
||||
|
||||
func (migration *migrateMetaresourcesTuples) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var storeID string
|
||||
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fetch all orgs.
|
||||
var orgIDs []string
|
||||
rows, err := tx.QueryContext(ctx, `SELECT id FROM organizations`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var orgID string
|
||||
if err := rows.Scan(&orgID); err != nil {
|
||||
return err
|
||||
}
|
||||
orgIDs = append(orgIDs, orgID)
|
||||
}
|
||||
|
||||
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
|
||||
|
||||
// Step 1: Delete all tuples with the old "metaresources" object_type.
|
||||
for _, orgID := range orgIDs {
|
||||
if isPG {
|
||||
_, err = tx.ExecContext(ctx, `DELETE FROM tuple WHERE store = ? AND object_type = ? AND object_id LIKE ?`,
|
||||
storeID, "metaresources", "organization/"+orgID+"/%")
|
||||
} else {
|
||||
_, err = tx.ExecContext(ctx, `DELETE FROM tuple WHERE store = ? AND object_type = ? AND object_id LIKE ?`,
|
||||
storeID, "metaresources", "organization/"+orgID+"/%")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Insert replacement tuples.
|
||||
// For types with their own FGA type (user, serviceaccount, role), create/list
|
||||
// go on the type directly. For all other resources, create/list go on "metaresource".
|
||||
// Also add new detach tuples for role/user/serviceaccount.
|
||||
tuples := []migrationTuple{
|
||||
// New detach tuples for admin
|
||||
{authtypes.SigNozAdminRoleName, "role", "role", "detach"},
|
||||
{authtypes.SigNozAdminRoleName, "serviceaccount", "serviceaccount", "detach"},
|
||||
// Replacement create/list for user/serviceaccount/role (moved from metaresources to own types)
|
||||
{authtypes.SigNozAdminRoleName, "serviceaccount", "serviceaccount", "create"},
|
||||
{authtypes.SigNozAdminRoleName, "serviceaccount", "serviceaccount", "list"},
|
||||
{authtypes.SigNozAdminRoleName, "role", "role", "create"},
|
||||
{authtypes.SigNozAdminRoleName, "role", "role", "list"},
|
||||
// Replacement create/list for resources that move from "metaresources" to "metaresource"
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "factor-api-key", "create"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "factor-api-key", "list"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "factor-api-key", "read"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "factor-api-key", "update"},
|
||||
{authtypes.SigNozAdminRoleName, "metaresource", "factor-api-key", "delete"},
|
||||
}
|
||||
|
||||
for _, orgID := range orgIDs {
|
||||
for _, tuple := range tuples {
|
||||
entropy := ulid.DefaultEntropy()
|
||||
now := time.Now().UTC()
|
||||
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
|
||||
|
||||
objectID := "organization/" + orgID + "/" + tuple.objectName + "/*"
|
||||
roleSubject := "organization/" + orgID + "/role/" + tuple.roleName
|
||||
|
||||
if isPG {
|
||||
user := "role:" + roleSubject + "#assignee"
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, user, "userset", tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
continue
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, user, "TUPLE_OPERATION_WRITE", tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", "userset", tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
continue
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
|
||||
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", 0, tupleID, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *migrateMetaresourcesTuples) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -200,7 +200,7 @@ func (t *telemetryMetaStore) getTracesKeys(ctx context.Context, fieldKeySelector
|
||||
`CASE
|
||||
// WHEN tagType = 'spanfield' THEN 1
|
||||
WHEN tagType = 'resource' THEN 2
|
||||
// WHEN tagType = 'scope' THEN 3
|
||||
WHEN tagType = 'scope' THEN 3
|
||||
WHEN tagType = 'tag' THEN 4
|
||||
ELSE 5
|
||||
END as priority`,
|
||||
|
||||
@@ -51,6 +51,7 @@ var (
|
||||
ValueType: schema.ColumnTypeString,
|
||||
}},
|
||||
"resource": {Name: "resource", Type: schema.JSONColumnType{}},
|
||||
"scope": {Name: "scope", Type: schema.JSONColumnType{}},
|
||||
|
||||
"events": {Name: "events", Type: schema.ArrayColumnType{
|
||||
ElementType: schema.ColumnTypeString,
|
||||
@@ -176,7 +177,7 @@ func (m *defaultFieldMapper) getColumn(
|
||||
case telemetrytypes.FieldContextResource:
|
||||
return []*schema.Column{indexV3Columns["resource"]}, nil
|
||||
case telemetrytypes.FieldContextScope:
|
||||
return []*schema.Column{}, qbtypes.ErrColumnNotFound
|
||||
return []*schema.Column{indexV3Columns["scope"]}, nil
|
||||
case telemetrytypes.FieldContextAttribute:
|
||||
switch key.FieldDataType {
|
||||
case telemetrytypes.FieldDataTypeString:
|
||||
@@ -261,21 +262,29 @@ func (m *defaultFieldMapper) FieldFor(
|
||||
|
||||
switch column.Type.GetType() {
|
||||
case schema.ColumnTypeEnumJSON:
|
||||
// json is only supported for resource context as of now
|
||||
if key.FieldContext != telemetrytypes.FieldContextResource {
|
||||
return "", errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "only resource context fields are supported for json columns, got %s", key.FieldContext.String)
|
||||
}
|
||||
oldColumn := indexV3Columns["resources_string"]
|
||||
oldKeyName := fmt.Sprintf("%s['%s']", oldColumn.Name, key.Name)
|
||||
// have to add ::string as clickHouse throws an error :- data types Variant/Dynamic are not allowed in GROUP BY
|
||||
// once clickHouse dependency is updated, we need to check if we can remove it.
|
||||
if key.Materialized {
|
||||
oldKeyName = telemetrytypes.FieldKeyToMaterializedColumnName(key)
|
||||
oldKeyNameExists := telemetrytypes.FieldKeyToMaterializedColumnNameForExists(key)
|
||||
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, %s==true, %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldKeyNameExists, oldKeyName), nil
|
||||
} else {
|
||||
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, mapContains(%s, '%s'), %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldColumn.Name, key.Name, oldKeyName), nil
|
||||
switch key.FieldContext {
|
||||
case telemetrytypes.FieldContextScope:
|
||||
scopeKey, _ := strings.CutPrefix(key.Name, "scope.") // required for support current implementation of select fields
|
||||
switch scopeKey {
|
||||
case "name", "version":
|
||||
return fmt.Sprintf("%s.%s::String", column.Name, scopeKey), nil
|
||||
default:
|
||||
return fmt.Sprintf("%s.attributes.`%s`::String", column.Name, scopeKey), nil
|
||||
}
|
||||
case telemetrytypes.FieldContextResource:
|
||||
oldColumn := indexV3Columns["resources_string"]
|
||||
oldKeyName := fmt.Sprintf("%s['%s']", oldColumn.Name, key.Name)
|
||||
// have to add ::string as clickHouse throws an error :- data types Variant/Dynamic are not allowed in GROUP BY
|
||||
// once clickHouse dependency is updated, we need to check if we can remove it.
|
||||
if key.Materialized {
|
||||
oldKeyName = telemetrytypes.FieldKeyToMaterializedColumnName(key)
|
||||
oldKeyNameExists := telemetrytypes.FieldKeyToMaterializedColumnNameForExists(key)
|
||||
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, %s==true, %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldKeyNameExists, oldKeyName), nil
|
||||
} else {
|
||||
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, mapContains(%s, '%s'), %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldColumn.Name, key.Name, oldKeyName), nil
|
||||
}
|
||||
}
|
||||
return "", errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "json column type only supported for resource and scope context, got %s", key.FieldContext.String)
|
||||
case schema.ColumnTypeEnumString,
|
||||
schema.ColumnTypeEnumUInt64,
|
||||
schema.ColumnTypeEnumUInt32,
|
||||
|
||||
@@ -78,6 +78,51 @@ func TestGetFieldKeyName(t *testing.T) {
|
||||
expectedResult: "multiIf(resource.`deployment.environment` IS NOT NULL, resource.`deployment.environment`::String, `resource_string_deployment$$environment_exists`==true, `resource_string_deployment$$environment`, NULL)",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Scope field - name (normalized)",
|
||||
key: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "name",
|
||||
FieldContext: telemetrytypes.FieldContextScope,
|
||||
},
|
||||
expectedResult: "scope.name::String",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Scope field - name (un-normalized)",
|
||||
key: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "scope.name",
|
||||
FieldContext: telemetrytypes.FieldContextScope,
|
||||
},
|
||||
expectedResult: "scope.name::String",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Scope field - version (normalized)",
|
||||
key: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "version",
|
||||
FieldContext: telemetrytypes.FieldContextScope,
|
||||
},
|
||||
expectedResult: "scope.version::String",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Scope field - version (un-normalized)",
|
||||
key: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "scope.version",
|
||||
FieldContext: telemetrytypes.FieldContextScope,
|
||||
},
|
||||
expectedResult: "scope.version::String",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Scope field - custom attribute",
|
||||
key: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "custom.attr",
|
||||
FieldContext: telemetrytypes.FieldContextScope,
|
||||
},
|
||||
expectedResult: "scope.attributes.`custom.attr`::String",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Non-existent column",
|
||||
key: telemetrytypes.TelemetryFieldKey{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user