Compare commits

..

20 Commits

Author SHA1 Message Date
Piyush Singariya
ac80e44782 Merge branch 'main' into fix/resource-query 2026-05-14 17:00:31 +05:30
Piyush Singariya
c141ac92c3 Merge branch 'main' into fix/resource-query 2026-05-14 16:59:00 +05:30
Piyush Singariya
9e3851af71 fix: comment fixed 2026-05-14 16:58:27 +05:30
Piyush Singariya
eb02171a81 Merge branch 'main' into fix/resource-query 2026-05-14 16:48:32 +05:30
Piyush Singariya
b0eceff9c6 fix: comment remove 2026-05-14 16:47:03 +05:30
Piyush Singariya
d8b61addd2 Merge branch 'main' into fix/resource-query 2026-05-14 16:28:44 +05:30
Piyush Singariya
e2927f6deb chore: bring in new fixture for building raw query 2026-05-14 16:28:13 +05:30
Piyush Singariya
9a8a70a66f fix: fmt py 2026-05-14 16:23:36 +05:30
Piyush Singariya
426095b713 chore: compressing tests into max 5 2026-05-14 16:19:14 +05:30
Piyush Singariya
c3058205b4 fix: uvx checks 2026-05-14 15:50:11 +05:30
Piyush Singariya
69e5977ab9 chore: comment fix 2026-05-14 15:33:22 +05:30
Piyush Singariya
19d04d005e chore: fmt py 2026-05-14 15:31:38 +05:30
Piyush Singariya
cee826f703 Merge branch 'main' into fix/resource-query 2026-05-14 15:30:21 +05:30
Piyush Singariya
5b9f864f6e chore: run non body tests in json enabled 2026-05-14 15:29:59 +05:30
Piyush Singariya
d24f0c13cc fix: package tests 2026-05-14 13:44:25 +05:30
Piyush Singariya
f70333630a test: add unit test for resource tags in json enabled flagger 2026-05-14 13:36:24 +05:30
Piyush Singariya
078b4c93d7 revert: stmt builder test changes 2026-05-14 13:19:30 +05:30
Piyush Singariya
9145f33ae8 Merge branch 'main' into fix/resource-query 2026-05-14 12:49:43 +05:30
Piyush Singariya
7bb67ba2cb fix: update test suite 2026-05-14 12:47:37 +05:30
Piyush Singariya
02311ede99 fix: query fix in conditionFor 2026-05-14 11:31:35 +05:30
74 changed files with 2975 additions and 2589 deletions

View File

@@ -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}"

View File

@@ -53,7 +53,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",

View File

@@ -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)

View File

@@ -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>
)}

View File

@@ -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>
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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>
);

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}

View File

@@ -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>
}

View File

@@ -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;
}

View File

@@ -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}`,

View File

@@ -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>

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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>
);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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;
}

View 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;
}
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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>
);

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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} />}
>

View File

@@ -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;
}

View File

@@ -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 = {

View File

@@ -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>
);

View File

@@ -1,11 +0,0 @@
.root {
height: 100%;
display: flex;
flex-direction: column;
}
.loadingSkeleton {
justify-content: center;
align-items: center;
padding: 20px;
}

View File

@@ -0,0 +1,11 @@
.trace-waterfall {
height: 100%;
display: flex;
flex-direction: column;
.loading-skeleton {
justify-content: center;
align-items: center;
padding: 20px;
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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} />

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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
/>,
);

View File

@@ -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/,

View File

@@ -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)}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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"

View File

@@ -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>

View File

@@ -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}

View File

@@ -41,7 +41,7 @@ func (c *conditionBuilder) conditionFor(
// TODO(Piyush): Update this to support multiple JSON columns based on evolutions
for _, column := range columns {
// TODO(Tushar): thread orgID here to evaluate correctly
if column.Type.GetType() == schema.ColumnTypeEnumJSON && c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})) && key.Name != messageSubField {
if column.Type.GetType() == schema.ColumnTypeEnumJSON && key.FieldContext == telemetrytypes.FieldContextBody && c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})) && key.Name != messageSubField {
valueType, value := InferDataType(value, operator, key)
cond, err := NewJSONConditionBuilder(key, valueType).buildJSONCondition(operator, value, sb)
if err != nil {

View File

@@ -33,7 +33,7 @@ func (t TestExpected) GetQuery() string {
}
func TestJSONStmtBuilder_TimeSeries(t *testing.T) {
statementBuilder := buildJSONTestStatementBuilder(t, false)
statementBuilder, _ := buildJSONTestStatementBuilder(t, false)
cases := []struct {
name string
@@ -171,7 +171,7 @@ func TestStmtBuilderTimeSeriesBodyGroupByPromoted(t *testing.T) {
*/
func TestJSONStmtBuilder_PrimitivePaths(t *testing.T) {
statementBuilder := buildJSONTestStatementBuilder(t, false)
statementBuilder, _ := buildJSONTestStatementBuilder(t, false)
cases := []struct {
name string
filter string
@@ -494,7 +494,7 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
*/
func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
statementBuilder := buildJSONTestStatementBuilder(t, false)
statementBuilder, _ := buildJSONTestStatementBuilder(t, false)
cases := []struct {
name string
filter string
@@ -799,7 +799,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
}
func TestJSONStmtBuilder_IndexedPaths(t *testing.T) {
statementBuilder := buildJSONTestStatementBuilder(t, true)
statementBuilder, _ := buildJSONTestStatementBuilder(t, true)
cases := []struct {
name string
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
@@ -918,7 +918,7 @@ func TestJSONStmtBuilder_IndexedPaths(t *testing.T) {
}
func TestJSONStmtBuilder_SelectField(t *testing.T) {
statementBuilder := buildJSONTestStatementBuilder(t, false)
statementBuilder, _ := buildJSONTestStatementBuilder(t, false)
cases := []struct {
name string
@@ -1006,7 +1006,7 @@ func TestJSONStmtBuilder_SelectField(t *testing.T) {
}
func TestJSONStmtBuilder_OrderBy(t *testing.T) {
statementBuilder := buildJSONTestStatementBuilder(t, false)
statementBuilder, _ := buildJSONTestStatementBuilder(t, false)
cases := []struct {
name string
@@ -1082,6 +1082,69 @@ func TestJSONStmtBuilder_OrderBy(t *testing.T) {
}
}
func TestResourceAggrAndGroupBy_WithJSONEnabled(t *testing.T) {
statementBuilder, metadataStore := buildJSONTestStatementBuilder(t, false)
releaseTime := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
keysMap := buildCompleteFieldKeyMap(releaseTime)
for _, keys := range keysMap {
for _, key := range keys {
metadataStore.SetKey(key)
}
}
cases := []struct {
name string
requestType qbtypes.RequestType
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
expected qbtypes.Statement
expectedErrContains string
}{
{
name: "resource_aggregation_and_group_by_with_json_enabled",
requestType: qbtypes.RequestTypeTimeSeries,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
GroupBy: []qbtypes.GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "region",
},
},
},
Filter: &qbtypes.Filter{
Expression: "user.name exists",
},
Aggregations: []qbtypes.LogAggregation{
{
Expression: "count_distinct(service.name)",
},
},
},
expected: qbtypes.Statement{
Query: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(resource.`region`::String IS NOT NULL, resource.`region`::String, NULL)) AS `region`, countDistinct(multiIf(resource.`service.name`::String IS NOT NULL, resource.`service.name`::String, NULL)) AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE ((dynamicElement(body_v2.`user.name`, 'String') IS NOT NULL) OR mapContains(attributes_string, 'user.name') = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts, `region`",
Args: []any{true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
Warnings: []string{"Key `user.name` is ambiguous, found 2 different combinations of field context / data type: [name=user.name,context=body,datatype=string name=user.name,context=attribute,datatype=string]."},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
if c.expectedErrContains != "" {
require.Error(t, err)
require.Contains(t, err.Error(), c.expectedErrContains)
} else {
require.NoError(t, err)
require.Equal(t, c.expected.Query, q.Query)
require.Equal(t, c.expected.Args, q.Args)
require.Equal(t, c.expected.Warnings, q.Warnings)
}
})
}
}
func buildTestTelemetryMetadataStore(t *testing.T, addIndexes bool) *telemetrytypestest.MockMetadataStore {
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
mockMetadataStore.SetStaticFields(IntrinsicFields)
@@ -1123,7 +1186,7 @@ func buildTestTelemetryMetadataStore(t *testing.T, addIndexes bool) *telemetryty
return mockMetadataStore
}
func buildJSONTestStatementBuilder(t *testing.T, addIndexes bool) *logQueryStatementBuilder {
func buildJSONTestStatementBuilder(t *testing.T, addIndexes bool) (*logQueryStatementBuilder, *telemetrytypestest.MockMetadataStore) {
t.Helper()
mockMetadataStore := buildTestTelemetryMetadataStore(t, addIndexes)
@@ -1144,5 +1207,5 @@ func buildJSONTestStatementBuilder(t *testing.T, addIndexes bool) *logQueryState
fl,
)
return statementBuilder
return statementBuilder, mockMetadataStore
}

View File

@@ -450,6 +450,35 @@ def build_scalar_query(
return {"type": "builder_query", "spec": spec}
def build_raw_query(
name: str,
signal: str,
*,
order: list[dict] | None = None,
limit: int | None = None,
filter_expression: str | None = None,
step_interval: int = DEFAULT_STEP_INTERVAL,
disabled: bool = False,
) -> dict:
spec: dict[str, Any] = {
"name": name,
"signal": signal,
"stepInterval": step_interval,
"disabled": disabled,
}
if order:
spec["order"] = order
if limit is not None:
spec["limit"] = limit
if filter_expression:
spec["filter"] = {"expression": filter_expression}
return {"type": "builder_query", "spec": spec}
def build_group_by_field(
name: str,
field_data_type: str = "string",

View File

@@ -11,6 +11,7 @@ from fixtures.logs import Logs
from fixtures.querier import (
build_logs_aggregation,
build_order_by,
build_raw_query,
build_scalar_query,
get_column_data_from_response,
get_rows,
@@ -27,28 +28,33 @@ def _run_query_case(signoz: types.SigNoz, token: str, now: datetime, case: dict[
start_ms = case.get("startMs", int((now - timedelta(seconds=10)).timestamp() * 1000))
end_ms = case.get("endMs", int(now.timestamp() * 1000))
aggregation = case.get("aggregation")
if aggregation and not isinstance(aggregation, list):
aggregations = [build_logs_aggregation(aggregation)]
elif aggregation:
aggregations = aggregation
if case["requestType"] == "raw":
query = build_raw_query(
name=case["name"],
signal="logs",
filter_expression=case.get("expression"),
order=case.get("order") or [build_order_by("timestamp", "desc")],
limit=case.get("limit", 100),
step_interval=case.get("stepInterval") or 60,
)
else:
aggregations = []
order = case.get("order")
if order is None and case["requestType"] == "raw":
order = [build_order_by("timestamp", "desc")]
query = build_scalar_query(
name=case["name"],
signal="logs",
aggregations=aggregations,
group_by=case.get("groupBy"),
order=order,
limit=case.get("limit", 100),
filter_expression=case.get("expression"),
step_interval=case.get("stepInterval") or 60,
)
aggregation = case.get("aggregation")
if aggregation and not isinstance(aggregation, list):
aggregations = [build_logs_aggregation(aggregation)]
elif aggregation:
aggregations = aggregation
else:
aggregations = []
query = build_scalar_query(
name=case["name"],
signal="logs",
aggregations=aggregations,
group_by=case.get("groupBy"),
order=case.get("order"),
limit=case.get("limit", 100),
filter_expression=case.get("expression"),
step_interval=case.get("stepInterval") or 60,
)
response = make_query_request(
signoz=signoz,
@@ -636,10 +642,9 @@ def test_select_order_by(
end_ms = int(now.timestamp() * 1000)
def _run(case: dict[str, Any]) -> None:
query = build_scalar_query(
query = build_raw_query(
name=case["name"],
signal="logs",
aggregations=[build_logs_aggregation("count()")],
order=case["order"],
limit=100,
step_interval=60,

View File

@@ -0,0 +1,285 @@
import json
from collections.abc import Callable
from datetime import UTC, datetime, timedelta
from typing import Any
import requests
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.logs import Logs
from fixtures.querier import (
build_group_by_field,
build_logs_aggregation,
build_order_by,
build_raw_query,
build_scalar_query,
get_rows,
get_scalar_table_data,
make_query_request,
)
def _raw(
signoz: types.SigNoz,
token: str,
start_ms: int,
end_ms: int,
name: str,
*,
expression: str | None = None,
order: list[dict] | None = None,
limit: int = 100,
) -> requests.Response:
q = build_raw_query(
name=name,
signal="logs",
filter_expression=expression,
order=order or [build_order_by("timestamp", "desc")],
limit=limit,
step_interval=60,
)
r = make_query_request(signoz, token, start_ms, end_ms, queries=[q], request_type="raw")
assert r.status_code == 200, f"HTTP {r.status_code} for '{name}': {r.text}"
return r
def _scalar(
signoz: types.SigNoz,
token: str,
start_ms: int,
end_ms: int,
name: str,
aggregation: str,
*,
expression: str | None = None,
group_by: list[dict] | None = None,
) -> requests.Response:
q = build_scalar_query(
name=name,
signal="logs",
aggregations=[build_logs_aggregation(aggregation)],
filter_expression=expression,
group_by=group_by,
step_interval=60,
)
r = make_query_request(signoz, token, start_ms, end_ms, queries=[q], request_type="scalar")
assert r.status_code == 200, f"HTTP {r.status_code} for '{name}': {r.text}"
return r
def _body_users(response: requests.Response) -> set[str | None]:
return {json.loads(row["data"]["body"]).get("user") for row in get_rows(response)}
def _body_scores(response: requests.Response) -> list[int | None]:
return [json.loads(row["data"]["body"]).get("score") for row in get_rows(response)]
def _services(response: requests.Response) -> list[str]:
return [row["data"]["resources_string"].get("service.name", "") for row in get_rows(response)]
def _counts(response: requests.Response) -> dict[str, Any]:
return {str(row[0]): row[-1] for row in get_scalar_table_data(response.json()) if row}
def _run_case(
signoz: types.SigNoz,
token: str,
start_ms: int,
end_ms: int,
case: dict[str, Any],
) -> None:
if case["requestType"] == "raw":
response = _raw(signoz, token, start_ms, end_ms, case["name"], expression=case.get("expression"), order=case.get("order"))
else:
response = _scalar(signoz, token, start_ms, end_ms, case["name"], case["aggregation"], expression=case.get("expression"), group_by=case.get("groupBy"))
assert case["validate"](response), f"Validation failed for '{case['name']}': {response.json()}"
# ============================================================================
# Filter · GroupBy · Aggregation — non-body fields across all three contexts
#
# Five cases, one dataset. Each case crosses a different combination of
# resource attr / log attr / top-level field in WHERE, GROUP BY, and agg:
#
# case 1 filter resource + log attr + top-level in WHERE (raw)
# case 2 group by resource × top-level multi-key (scalar)
# case 3 aggregation count_distinct(log attr) grouped by top-level (scalar)
# case 4 agg+filter count by resource, body-field WHERE guard (scalar)
# case 5 agg+filter count_distinct(resource) by log attr, top-level filter (scalar)
#
# Data landscape (5 logs):
# log1 — auth-svc, GET, INFO, score=80, user=alice
# log2 — auth-svc, POST, ERROR, score=90, user=bob
# log3 — auth-svc, GET, INFO, score=60, user=carol
# log4 — api-gw, GET, WARN, score=70, user=diana
# log5 — worker, DELETE, ERROR, score=100, user=eve
# ============================================================================
def test_non_body_filter_groupby_aggregation(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_logs: Callable[[list[Logs]], None],
export_json_types: Callable[[list[Logs]], None],
) -> None:
now = datetime.now(tz=UTC)
start_ms = int((now - timedelta(seconds=10)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
log_data = [
("auth-svc", "GET", "INFO", {"score": 80, "user": "alice"}),
("auth-svc", "POST", "ERROR", {"score": 90, "user": "bob"}),
("auth-svc", "GET", "INFO", {"score": 60, "user": "carol"}),
("api-gw", "GET", "WARN", {"score": 70, "user": "diana"}),
("worker", "DELETE", "ERROR", {"score": 100, "user": "eve"}),
]
logs_list = [
Logs(
timestamp=now - timedelta(seconds=len(log_data) - i),
resources={"service.name": svc},
attributes={"http.method": method},
body_v2=json.dumps(body),
body_promoted="",
severity_text=sev,
)
for i, (svc, method, sev, body) in enumerate(log_data)
]
export_json_types(logs_list)
insert_logs(logs_list)
token = get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)
cases = [
# 1. Filter — resource + log attr + top-level in WHERE (all three non-body contexts at once)
{
"name": "filter.cross_context",
"requestType": "raw",
"expression": 'service.name = "auth-svc" AND http.method = "GET" AND severity_text = "INFO"',
"validate": lambda r: len(get_rows(r)) == 2 and _body_users(r) == {"alice", "carol"},
},
# 2. GroupBy — resource × top-level multi-key, no filter
# Proves both contexts resolve correctly as simultaneous GROUP BY keys.
{
"name": "groupby.resource_x_toplevel",
"requestType": "scalar",
"expression": None,
"groupBy": [build_group_by_field("service.name"), {"name": "severity_text"}],
"aggregation": "count()",
# auth-svc+INFO=2, auth-svc+ERROR=1, api-gw+WARN=1, worker+ERROR=1
"validate": lambda r: (p := {(str(row[0]), str(row[1])): row[-1] for row in get_scalar_table_data(r.json()) if len(row) >= 3}) and p.get(("auth-svc", "INFO")) == 2 and p.get(("auth-svc", "ERROR")) == 1 and p.get(("api-gw", "WARN")) == 1 and p.get(("worker", "ERROR")) == 1,
},
# 3. Aggregation — count_distinct(log attr) grouped by top-level
# ERROR logs use {POST, DELETE} → 2 distinct methods; INFO/WARN use only GET → 1.
{
"name": "agg.count_distinct_attr_by_toplevel",
"requestType": "scalar",
"expression": None,
"groupBy": [{"name": "severity_text"}],
"aggregation": "count_distinct(http.method)",
"validate": lambda r: (rows := _counts(r)) and int(rows["INFO"]) == 1 and int(rows["ERROR"]) == 2 and int(rows["WARN"]) == 1,
},
# 4. Aggregation + body filter — count by resource WHERE body score >= 80
# Body field gates the logs; non-body field drives the GROUP BY.
{
"name": "agg.count_by_resource_body_filter",
"requestType": "scalar",
"expression": "score >= 80",
"groupBy": [build_group_by_field("service.name")],
"aggregation": "count()",
# score>=80: alice(80), bob(90), eve(100) → auth-svc: 2, worker: 1; api-gw excluded
"validate": lambda r: (rows := _counts(r)) and int(rows["auth-svc"]) == 2 and int(rows["worker"]) == 1 and "api-gw" not in rows,
},
# 5. Aggregation + top-level filter — count_distinct(resource) grouped by log attr
# Aggregates a resource attr, groups by a log attr, filtered by a top-level field.
{
"name": "agg.count_distinct_resource_by_attr_toplevel_filter",
"requestType": "scalar",
"expression": "severity_text IN ['INFO', 'WARN']",
"groupBy": [{"name": "http.method"}],
"aggregation": "count_distinct(service.name)",
# INFO/WARN logs: GET(auth-svc×2, api-gw) → 2 distinct svcs; POST/DELETE excluded
"validate": lambda r: (rows := _counts(r)) and int(rows["GET"]) == 2 and "POST" not in rows and "DELETE" not in rows,
},
]
for case in cases:
case.setdefault("groupBy", None)
_run_case(signoz, token, start_ms, end_ms, case)
# ============================================================================
# OrderBy — non-body fields as primary sort keys
#
# Four cases cover every non-body context as the primary ORDER BY key:
# orderby.service_asc resource attr (service.name ASC)
# orderby.timestamp_desc top-level (timestamp DESC)
# orderby.severity_asc top-level (severity_text ASC)
# orderby.multi_method_then_score log attr primary, body path secondary
#
# Data landscape:
# log1 — svc-a, GET, INFO, score=80, ts=now-4s
# log2 — svc-a, POST, INFO, score=90, ts=now-3s
# log3 — svc-b, GET, WARN, score=60, ts=now-2s
# log4 — svc-b, DELETE, WARN, score=70, ts=now-1s
# ============================================================================
def test_non_body_orderby(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_logs: Callable[[list[Logs]], None],
export_json_types: Callable[[list[Logs]], None],
) -> None:
now = datetime.now(tz=UTC)
start_ms = int((now - timedelta(seconds=10)).timestamp() * 1000)
end_ms = int(now.timestamp() * 1000)
logs_list = [
Logs(timestamp=now - timedelta(seconds=4), resources={"service.name": "svc-a"}, attributes={"http.method": "GET"}, body_v2=json.dumps({"score": 80}), body_promoted="", severity_text="INFO"),
Logs(timestamp=now - timedelta(seconds=3), resources={"service.name": "svc-a"}, attributes={"http.method": "POST"}, body_v2=json.dumps({"score": 90}), body_promoted="", severity_text="INFO"),
Logs(timestamp=now - timedelta(seconds=2), resources={"service.name": "svc-b"}, attributes={"http.method": "GET"}, body_v2=json.dumps({"score": 60}), body_promoted="", severity_text="WARN"),
Logs(timestamp=now - timedelta(seconds=1), resources={"service.name": "svc-b"}, attributes={"http.method": "DELETE"}, body_v2=json.dumps({"score": 70}), body_promoted="", severity_text="WARN"),
]
export_json_types(logs_list)
insert_logs(logs_list)
token = get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)
cases = [
# resource attr ASC: svc-a×2 before svc-b×2
{
"name": "orderby.service_asc",
"requestType": "raw",
"order": [build_order_by("service.name", "asc")],
"validate": lambda r: len(get_rows(r)) == 4 and _services(r)[:2] == ["svc-a", "svc-a"] and _services(r)[2:] == ["svc-b", "svc-b"],
},
# top-level timestamp DESC: ts-1s(svc-b/70), ts-2s(svc-b/60), ts-3s(svc-a/90), ts-4s(svc-a/80)
{
"name": "orderby.timestamp_desc",
"requestType": "raw",
"order": [build_order_by("timestamp", "desc")],
"validate": lambda r: len(get_rows(r)) == 4 and _body_scores(r) == [70, 60, 90, 80] and _services(r) == ["svc-b", "svc-b", "svc-a", "svc-a"],
},
# top-level severity_text ASC: INFO(svc-a×2) before WARN(svc-b×2)
{
"name": "orderby.severity_asc",
"requestType": "raw",
"order": [build_order_by("severity_text", "asc")],
"validate": lambda r: len(get_rows(r)) == 4 and _services(r)[:2] == ["svc-a", "svc-a"] and _services(r)[2:] == ["svc-b", "svc-b"],
},
# multi-key: http.method ASC then score ASC — DELETE(70), GET(60,80), POST(90)
{
"name": "orderby.multi_method_then_score",
"requestType": "raw",
"order": [build_order_by("http.method", "asc"), build_order_by("score", "asc")],
# DELETE < GET < POST alphabetically; within GET scores go 60→80
"validate": lambda r: len(get_rows(r)) == 4 and _body_scores(r) == [70, 60, 80, 90],
},
]
for case in cases:
case.setdefault("groupBy", None)
_run_case(signoz, token, start_ms, end_ms, case)