mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-14 14:10:32 +01:00
Compare commits
20 Commits
feat/trace
...
fix/resour
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac80e44782 | ||
|
|
c141ac92c3 | ||
|
|
9e3851af71 | ||
|
|
eb02171a81 | ||
|
|
b0eceff9c6 | ||
|
|
d8b61addd2 | ||
|
|
e2927f6deb | ||
|
|
9a8a70a66f | ||
|
|
426095b713 | ||
|
|
c3058205b4 | ||
|
|
69e5977ab9 | ||
|
|
19d04d005e | ||
|
|
cee826f703 | ||
|
|
5b9f864f6e | ||
|
|
d24f0c13cc | ||
|
|
f70333630a | ||
|
|
078b4c93d7 | ||
|
|
9145f33ae8 | ||
|
|
7bb67ba2cb | ||
|
|
02311ede99 |
@@ -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}"
|
||||
|
||||
@@ -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",
|
||||
|
||||
76
frontend/pnpm-lock.yaml
generated
76
frontend/pnpm-lock.yaml
generated
@@ -89,8 +89,8 @@ importers:
|
||||
specifier: 0.0.2
|
||||
version: 0.0.2(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@signozhq/ui':
|
||||
specifier: 0.0.19
|
||||
version: 0.0.19(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.27.0(react@18.2.0))(react@18.2.0)
|
||||
specifier: 0.0.18
|
||||
version: 0.0.18(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.27.0(react@18.2.0))(react@18.2.0)
|
||||
'@tanstack/react-table':
|
||||
specifier: 8.21.3
|
||||
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -1907,105 +1907,89 @@ packages:
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
@@ -2360,56 +2344,48 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-arm64-musl@0.47.0':
|
||||
resolution: {integrity: sha512-IxtQC/sbBi4ubbY+MdwdanRWrG9InQJVZqyMsBa5IUaQcnSg86gQme574HxXMC1p4bo4YhV99zQ+wNnGCvEgzw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.47.0':
|
||||
resolution: {integrity: sha512-EWXEhOMbWO0q6eJSbu0QLkU8cKi0ljlYLngeDs2Ocu/pm1rrLwyQiYzlFbdnMRURI4w9ndr1sI9rSbhlJ5o23Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.47.0':
|
||||
resolution: {integrity: sha512-tZrjS11TUiDuEpRaqdk8K9F9xETRyKXfuZKmdeW+Gj7coBnm7+8sBEfyt033EAFEQSlkniAXvBLh+Qja2ioGBQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.47.0':
|
||||
resolution: {integrity: sha512-KBFy+2CFKUCZzYwX2ZOPQKck1vjQbz+hextuc19G4r0WRJwadfAeuQMQRQvB+Ivc8brlbOVg7et8K7E467440g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.47.0':
|
||||
resolution: {integrity: sha512-REUPFKVGSiK99B+9eaPhluEVglzaoj/SMykNC5SUiV2RSsBfV5lWN7Y0iCIc251Wz3GaeAGZsJ/zj3gjarxdFg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-gnu@0.47.0':
|
||||
resolution: {integrity: sha512-KVftVSVEDeIfRW3TIeLe3aNI/iY4m1fu5mDwHcisKMZSCMKLkrhFsjowC7o9RoqNPxbbglm2+/6KAKBIts2t0Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-musl@0.47.0':
|
||||
resolution: {integrity: sha512-DTsmGEaA2860Aq5VUyDO8/MT9NFxwVL93RnRYmpMwK6DsSkThmvEpqoUDDljziEpAedMRG19SCogrNbINSbLUQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-openharmony-arm64@0.47.0':
|
||||
resolution: {integrity: sha512-8r5BDro7fLOBoq1JXHLVSs55OlrxQhEso4HVo0TcY7OXJUPYfjPoOaYL5us+yIwqyP9rQwN+rxuiNFSmaxSuOQ==}
|
||||
@@ -2512,56 +2488,48 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-arm64-musl@1.62.0':
|
||||
resolution: {integrity: sha512-8eCy3FCDuWUM5hWujAv6heMvfZPbcCOU3SdQUAkixZLu5bSzOkNfirJiLGoQFO943xceOKkiQRMQNzH++jM3WA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.62.0':
|
||||
resolution: {integrity: sha512-NjQ7K7tpTPDe9J+yq8p/s/J0E7lRCkK2uDBDqvT4XIT6f4Z0tlnr59OBg/WcrmVHER1AbrcfyxhGTXgcG8ytWg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.62.0':
|
||||
resolution: {integrity: sha512-oKZed9gmSwze29dEt3/Wnsv6l/Ygw/FUst+8Kfpv2SGeS/glEoTGZAMQw37SVyzFV76UTHJN2snGgxK2t2+8ow==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-musl@1.62.0':
|
||||
resolution: {integrity: sha512-gBjBxQ+9lGpAYq+ELqw0w8QXsBnkZclFc7GRX2r0LnEVn3ZTEqeIKpKcGjucmp76Q53bvJD0i4qBWBhcfhSfGA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-s390x-gnu@1.62.0':
|
||||
resolution: {integrity: sha512-Ew2Kxs9EQ9/mbAIJ2hvocMC0wsOu6YKzStI2eFBDt+Td5O8seVC/oxgRIHqCcl5sf5ratA1nozQBAuv7tphkHg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-gnu@1.62.0':
|
||||
resolution: {integrity: sha512-5z25jcAA0gfKyVwz71A0VXgaPlocPoTAxhlv/hgoK6tlCrfoNuw7haWbDHvGMfjXhdic4EqVXGRv5XsTqFnbRQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-musl@1.62.0':
|
||||
resolution: {integrity: sha512-IWpHmMB6ZDllPvqWDkG6AmXrN7JF5e/c4g/0PuURsmlK+vHoYZPB70rr4u1bn3I4LsKCSpqqfveyx6UCOC8wdg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-openharmony-arm64@1.62.0':
|
||||
resolution: {integrity: sha512-fjlSxxrD5pA594vkyikCS9MnPRjQawW6/BLgyTYkO+73wwPlYjkcZ7LSd974l0Q2zkHQmu4DPvJFLYA7o8xrxQ==}
|
||||
@@ -2616,42 +2584,36 @@ packages:
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
|
||||
@@ -3512,28 +3474,24 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==}
|
||||
@@ -3686,8 +3644,8 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18.2.0
|
||||
|
||||
'@signozhq/ui@0.0.19':
|
||||
resolution: {integrity: sha512-2q6aRxN/PR4PlR2xJZAREEuvLPiDFggfFKzCW2Z5vHVVbrgnvZHWD1jPUuwszfEg0ceH3UvkwqceO7wN4uRJAA==}
|
||||
'@signozhq/ui@0.0.18':
|
||||
resolution: {integrity: sha512-1p3ALh76kafiz5yX7ReNKVcHDt2od7CcZD/Vx9i2adTwTeynkLJcEfVoXoJD3oh1kKTleooOiOjRyxlA7VzmSA==}
|
||||
peerDependencies:
|
||||
'@signozhq/icons': 0.3.0
|
||||
react: ^18.2.0
|
||||
@@ -4308,49 +4266,41 @@ packages:
|
||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||
@@ -4417,7 +4367,7 @@ packages:
|
||||
resolution: {integrity: sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
vite: npm:rolldown-vite@7.3.1
|
||||
|
||||
'@webassemblyjs/ast@1.14.1':
|
||||
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
|
||||
@@ -7244,28 +7194,24 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.31.1:
|
||||
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.31.1:
|
||||
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}
|
||||
@@ -10293,7 +10239,7 @@ packages:
|
||||
oxlint: '>=1'
|
||||
stylelint: '>=16'
|
||||
typescript: '*'
|
||||
vite: '>=5.4.21'
|
||||
vite: npm:rolldown-vite@7.3.1
|
||||
vls: '*'
|
||||
vti: '*'
|
||||
vue-tsc: ~2.2.10 || ^3.0.0
|
||||
@@ -10322,12 +10268,12 @@ packages:
|
||||
vite-plugin-compression@0.5.1:
|
||||
resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==}
|
||||
peerDependencies:
|
||||
vite: '>=2.0.0'
|
||||
vite: npm:rolldown-vite@7.3.1
|
||||
|
||||
vite-plugin-html@3.2.2:
|
||||
resolution: {integrity: sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q==}
|
||||
peerDependencies:
|
||||
vite: '>=2.0.0'
|
||||
vite: npm:rolldown-vite@7.3.1
|
||||
|
||||
vite-plugin-image-optimizer@2.0.3:
|
||||
resolution: {integrity: sha512-1vrFOTcpSvv6DCY7h8UXab4wqMAjTJB/ndOzG/Kmj1oDOuPF6mbjkNQoGzzCEYeWGe7qU93jc8oQqvoJ57al3A==}
|
||||
@@ -10335,7 +10281,7 @@ packages:
|
||||
peerDependencies:
|
||||
sharp: '>=0.34.0'
|
||||
svgo: '>=4'
|
||||
vite: '>=5'
|
||||
vite: npm:rolldown-vite@7.3.1
|
||||
peerDependenciesMeta:
|
||||
sharp:
|
||||
optional: true
|
||||
@@ -10345,7 +10291,7 @@ packages:
|
||||
vite-tsconfig-paths@6.1.1:
|
||||
resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==}
|
||||
peerDependencies:
|
||||
vite: '*'
|
||||
vite: npm:rolldown-vite@7.3.1
|
||||
|
||||
void-elements@3.1.0:
|
||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||
@@ -13955,7 +13901,7 @@ snapshots:
|
||||
- react-dom
|
||||
- tailwindcss
|
||||
|
||||
'@signozhq/ui@0.0.19(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.27.0(react@18.2.0))(react@18.2.0)':
|
||||
'@signozhq/ui@0.0.18(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.27.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@chenglou/pretext': 0.0.5
|
||||
'@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { Drawer } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { Maximize2, MessageSquare, Plus, X } from '@signozhq/icons';
|
||||
@@ -52,7 +52,7 @@ export default function AIAssistantDrawer(): JSX.Element {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TooltipSimple title="New conversation">
|
||||
<Tooltip title="New conversation">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -62,9 +62,9 @@ export default function AIAssistantDrawer(): JSX.Element {
|
||||
>
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Open full screen">
|
||||
<Tooltip title="Open full screen">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -75,9 +75,9 @@ export default function AIAssistantDrawer(): JSX.Element {
|
||||
>
|
||||
<Maximize2 size={16} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Close">
|
||||
<Tooltip title="Close">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -87,7 +87,7 @@ export default function AIAssistantDrawer(): JSX.Element {
|
||||
>
|
||||
<X size={16} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { History, Maximize2, Minus, Plus, Sparkles, X } from '@signozhq/icons';
|
||||
|
||||
@@ -132,7 +132,7 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<TooltipSimple title={showHistory ? 'Back to chat' : 'Conversations'}>
|
||||
<Tooltip title={showHistory ? 'Back to chat' : 'Conversations'}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -142,9 +142,9 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
>
|
||||
<History size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="New conversation">
|
||||
<Tooltip title="New conversation">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -153,9 +153,9 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Open full screen">
|
||||
<Tooltip title="Open full screen">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -165,9 +165,9 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Minimize to side panel">
|
||||
<Tooltip title="Minimize to side panel">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -176,9 +176,9 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
>
|
||||
<Minus size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Close">
|
||||
<Tooltip title="Close">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -187,7 +187,7 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { matchPath, useHistory, useLocation } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { History, Maximize2, Plus, Sparkles, X } from '@signozhq/icons';
|
||||
|
||||
@@ -125,7 +125,7 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<TooltipSimple title={showHistory ? 'Back to chat' : 'Conversations'}>
|
||||
<Tooltip title={showHistory ? 'Back to chat' : 'Conversations'}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -135,9 +135,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
>
|
||||
<History size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="New conversation">
|
||||
<Tooltip title="New conversation">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -147,9 +147,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Open full screen">
|
||||
<Tooltip title="Open full screen">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -160,9 +160,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Close">
|
||||
<Tooltip title="Close">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -172,7 +172,7 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { Bot } from '@signozhq/icons';
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function AIAssistantTrigger(): JSX.Element | null {
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipSimple title="AI Assistant">
|
||||
<Tooltip title="AI Assistant">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
@@ -40,6 +40,6 @@ export default function AIAssistantTrigger(): JSX.Element | null {
|
||||
>
|
||||
<Bot size={20} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import cx from 'classnames';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import type { MessageActionDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
|
||||
import {
|
||||
ApplyFilterSignalDTO,
|
||||
@@ -524,9 +524,9 @@ export default function ActionsSection({
|
||||
);
|
||||
|
||||
return tooltip ? (
|
||||
<TooltipSimple key={key} title={tooltip}>
|
||||
<Tooltip key={key} title={tooltip}>
|
||||
{chip}
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span key={key}>{chip}</span>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Badge } from '@signozhq/ui/badge';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import type { UploadFile } from 'antd';
|
||||
import {
|
||||
getListRulesQueryKey,
|
||||
@@ -899,7 +899,7 @@ export default function ChatInput({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<TooltipSimple title="Voice input">
|
||||
<Tooltip title="Voice input">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -910,11 +910,11 @@ export default function ChatInput({
|
||||
>
|
||||
<Mic size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
))}
|
||||
|
||||
{isStreaming && onCancel ? (
|
||||
<TooltipSimple title="Stop generating">
|
||||
<Tooltip title="Stop generating">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="icon"
|
||||
@@ -924,7 +924,7 @@ export default function ChatInput({
|
||||
>
|
||||
<Square size={10} fill="currentColor" strokeWidth={0} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button
|
||||
variant="solid"
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { Plus, Search } from '@signozhq/icons';
|
||||
|
||||
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
|
||||
@@ -157,7 +157,7 @@ export default function ConversationsList({
|
||||
{isLoadingThreads && <HeaderLoadingDots />}
|
||||
|
||||
{!isLoadingThreads && showAddNewConversation && (
|
||||
<TooltipSimple title="New conversation">
|
||||
<Tooltip title="New conversation">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="sm"
|
||||
@@ -167,7 +167,7 @@ export default function ConversationsList({
|
||||
>
|
||||
<Plus size={12} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import cx from 'classnames';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { Check, Copy, RefreshCw, ThumbsDown, ThumbsUp } from '@signozhq/icons';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
@@ -126,7 +126,7 @@ export default function MessageFeedback({
|
||||
<>
|
||||
<div className={cx(styles.feedback, { [styles.visible]: isLastAssistant })}>
|
||||
<div className={styles.actions}>
|
||||
<TooltipSimple title={copied ? 'Copied!' : 'Copy'}>
|
||||
<Tooltip title={copied ? 'Copied!' : 'Copy'}>
|
||||
<Button
|
||||
className={styles.btn}
|
||||
size="icon"
|
||||
@@ -136,9 +136,9 @@ export default function MessageFeedback({
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Good response">
|
||||
<Tooltip title="Good response">
|
||||
<Button
|
||||
className={cx(styles.btn, { [styles.votedUp]: vote === 'positive' })}
|
||||
size="icon"
|
||||
@@ -148,9 +148,9 @@ export default function MessageFeedback({
|
||||
>
|
||||
<ThumbsUp size={12} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
<TooltipSimple title="Bad response">
|
||||
<Tooltip title="Bad response">
|
||||
<Button
|
||||
className={cx(styles.btn, {
|
||||
[styles.votedDown]: vote === 'negative',
|
||||
@@ -162,10 +162,10 @@ export default function MessageFeedback({
|
||||
>
|
||||
<ThumbsDown size={12} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
|
||||
{onRegenerate && (
|
||||
<TooltipSimple title="Regenerate">
|
||||
<Tooltip title="Regenerate">
|
||||
<Button
|
||||
className={styles.btn}
|
||||
size="icon"
|
||||
@@ -175,7 +175,7 @@ export default function MessageFeedback({
|
||||
>
|
||||
<RefreshCw size={12} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { Check, Copy } from '@signozhq/icons';
|
||||
|
||||
import { Message } from '../../types';
|
||||
@@ -32,7 +32,7 @@ export default function UserMessageActions({
|
||||
|
||||
return (
|
||||
<div className={styles.actions}>
|
||||
<TooltipSimple title={copied ? 'Copied!' : 'Copy'}>
|
||||
<Tooltip title={copied ? 'Copied!' : 'Copy'}>
|
||||
<Button
|
||||
className={styles.btn}
|
||||
size="icon"
|
||||
@@ -42,7 +42,7 @@ export default function UserMessageActions({
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
.body {
|
||||
padding: 12px 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--l1-background);
|
||||
|
||||
// TabsRoot — last direct child div
|
||||
> div:last-child {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[role='tablist'] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Tabs library wrapper — scoped by `.body` so the global match is contained.
|
||||
:global([class*='tabs__list-wrapper']) {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tabsScroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr;
|
||||
gap: 4px 8px;
|
||||
padding: 8px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.serviceName {
|
||||
font-size: 13px;
|
||||
color: var(--l1-foreground);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.barCell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: var(--l3-background);
|
||||
border-radius: 3px;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.barFill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.valueWide {
|
||||
min-width: 55px;
|
||||
}
|
||||
|
||||
.valueNarrow {
|
||||
min-width: 25px;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
.analytics-panel {
|
||||
&__body {
|
||||
padding: 12px 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--l1-background);
|
||||
|
||||
// TabsRoot — last direct child div
|
||||
> div:last-child {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[role='tablist'] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__tabs-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr;
|
||||
gap: 4px 8px;
|
||||
padding: 8px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&__service-name {
|
||||
font-size: 13px;
|
||||
color: var(--l1-foreground);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&__bar-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: var(--l3-background);
|
||||
border-radius: 3px;
|
||||
min-width: 40px;
|
||||
|
||||
&--small {
|
||||
max-width: 80px;
|
||||
flex: 0 0 80px;
|
||||
}
|
||||
}
|
||||
|
||||
&__bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&__value {
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&--wide {
|
||||
min-width: 55px;
|
||||
}
|
||||
|
||||
&--narrow {
|
||||
min-width: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs root
|
||||
[class*='tabs__list-wrapper'] {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
TabsRoot,
|
||||
TabsTrigger,
|
||||
} from '@signozhq/ui/tabs';
|
||||
import cx from 'classnames';
|
||||
import { DetailsHeader } from 'components/DetailsPanel';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
@@ -17,7 +16,7 @@ import {
|
||||
getAggregationMap as findAggregationMap,
|
||||
} from '../../utils/aggregations';
|
||||
|
||||
import styles from './AnalyticsPanel.module.scss';
|
||||
import './AnalyticsPanel.styles.scss';
|
||||
|
||||
interface AnalyticsPanelProps {
|
||||
isOpen: boolean;
|
||||
@@ -87,6 +86,7 @@ function AnalyticsPanel({
|
||||
return (
|
||||
<FloatingPanel
|
||||
isOpen
|
||||
className="analytics-panel"
|
||||
width={PANEL_WIDTH}
|
||||
height={window.innerHeight - PANEL_MARGIN_TOP - PANEL_MARGIN_BOTTOM}
|
||||
defaultPosition={{
|
||||
@@ -110,7 +110,7 @@ function AnalyticsPanel({
|
||||
className="floating-panel__drag-handle"
|
||||
/>
|
||||
|
||||
<div className={styles.body}>
|
||||
<div className="analytics-panel__body">
|
||||
<TabsRoot defaultValue="exec-time">
|
||||
<TabsList variant="secondary">
|
||||
<TabsTrigger value="exec-time" variant="secondary">
|
||||
@@ -121,30 +121,33 @@ function AnalyticsPanel({
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className={styles.tabsScroll}>
|
||||
<div className="analytics-panel__tabs-scroll">
|
||||
<TabsContent value="exec-time">
|
||||
<div className={styles.list}>
|
||||
<div className="analytics-panel__list">
|
||||
{execTimeRows.map((row) => (
|
||||
<>
|
||||
<div
|
||||
key={`${row.group}-dot`}
|
||||
className={styles.dot}
|
||||
className="analytics-panel__dot"
|
||||
style={{ backgroundColor: row.color }}
|
||||
/>
|
||||
<span key={`${row.group}-name`} className={styles.serviceName}>
|
||||
<span
|
||||
key={`${row.group}-name`}
|
||||
className="analytics-panel__service-name"
|
||||
>
|
||||
{row.group}
|
||||
</span>
|
||||
<div key={`${row.group}-bar`} className={styles.barCell}>
|
||||
<div className={styles.bar}>
|
||||
<div key={`${row.group}-bar`} className="analytics-panel__bar-cell">
|
||||
<div className="analytics-panel__bar">
|
||||
<div
|
||||
className={styles.barFill}
|
||||
className="analytics-panel__bar-fill"
|
||||
style={{
|
||||
width: `${Math.min(row.percentage, 100)}%`,
|
||||
backgroundColor: row.color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className={cx(styles.value, styles.valueWide)}>
|
||||
<span className="analytics-panel__value analytics-panel__value--wide">
|
||||
{row.percentage.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
@@ -154,28 +157,31 @@ function AnalyticsPanel({
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="spans">
|
||||
<div className={styles.list}>
|
||||
<div className="analytics-panel__list">
|
||||
{spanCountRows.map((row) => (
|
||||
<>
|
||||
<div
|
||||
key={`${row.group}-dot`}
|
||||
className={styles.dot}
|
||||
className="analytics-panel__dot"
|
||||
style={{ backgroundColor: row.color }}
|
||||
/>
|
||||
<span key={`${row.group}-name`} className={styles.serviceName}>
|
||||
<span
|
||||
key={`${row.group}-name`}
|
||||
className="analytics-panel__service-name"
|
||||
>
|
||||
{row.group}
|
||||
</span>
|
||||
<div key={`${row.group}-bar`} className={styles.barCell}>
|
||||
<div className={styles.bar}>
|
||||
<div key={`${row.group}-bar`} className="analytics-panel__bar-cell">
|
||||
<div className="analytics-panel__bar">
|
||||
<div
|
||||
className={styles.barFill}
|
||||
className="analytics-panel__bar-fill"
|
||||
style={{
|
||||
width: `${(row.count / row.max) * 100}%`,
|
||||
backgroundColor: row.color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className={cx(styles.value, styles.valueNarrow)}>
|
||||
<span className="analytics-panel__value analytics-panel__value--narrow">
|
||||
{row.count}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
.linked-spans {
|
||||
position: relative;
|
||||
|
||||
&__toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
&__label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__chevron {
|
||||
transition: transform 0.15s ease;
|
||||
|
||||
&--open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { Badge } from '@signozhq/ui/badge';
|
||||
import ROUTES from 'constants/routes';
|
||||
import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
||||
|
||||
import styles from './LinkedSpans.module.scss';
|
||||
import './LinkedSpans.styles.scss';
|
||||
|
||||
interface SpanReference {
|
||||
traceId: string;
|
||||
@@ -56,12 +56,12 @@ export function LinkedSpansToggle({
|
||||
toggleOpen: () => void;
|
||||
}): JSX.Element {
|
||||
if (count === 0) {
|
||||
return <span className={styles.label}>0 linked spans</span>;
|
||||
return <span className="linked-spans__label">0 linked spans</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button type="button" className={styles.toggle} onClick={toggleOpen}>
|
||||
<span className={styles.label}>
|
||||
<button type="button" className="linked-spans__toggle" onClick={toggleOpen}>
|
||||
<span className="linked-spans__label">
|
||||
{count} linked span{count !== 1 ? 's' : ''}
|
||||
</span>
|
||||
{isOpen ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
@@ -87,7 +87,7 @@ export function LinkedSpansPanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.list}>
|
||||
<div className="linked-spans__list">
|
||||
{linkedSpans.map((item) => (
|
||||
<KeyValueLabel
|
||||
key={item.spanId}
|
||||
@@ -108,7 +108,7 @@ function LinkedSpans({ references }: LinkedSpansProps): JSX.Element {
|
||||
const { linkedSpans, count, isOpen, toggleOpen } = useLinkedSpans(references);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className="linked-spans">
|
||||
<LinkedSpansToggle count={count} isOpen={isOpen} toggleOpen={toggleOpen} />
|
||||
<LinkedSpansPanel linkedSpans={linkedSpans} isOpen={isOpen} />
|
||||
</div>
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
.root {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
background: var(--l1-background);
|
||||
font-size: 14px;
|
||||
gap: 16px;
|
||||
|
||||
// DataViewer keeps its global `.data-viewer` class — give it a min-height
|
||||
// so the tab area doesn't collapse on short content.
|
||||
:global(.data-viewer) {
|
||||
min-height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.detailsSection {
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tabsSection {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-height: 100%;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
// TabsRoot — direct child of tabs-section
|
||||
> div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[role='tablist'] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[class*='tabs__list-wrapper'] {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tabsScroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
[role='tabpanel'] {
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.spanRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.spanInfo {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 16px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.spanInfoItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--l2-foreground);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.highlightedOptions {
|
||||
padding: 8px 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0;
|
||||
|
||||
// KeyValueLabel uses a global `.key-value-label` root; constrain it
|
||||
// inside the two-column grid so values can ellipsize cleanly.
|
||||
:global(.key-value-label) {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.serviceDot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-forest);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.traceId {
|
||||
color: var(--accent-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.traceIdCopy {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
// Tooltip is rendered in a portal but the SpanDetailsPanel can be docked as a
|
||||
// FloatingPanel (z-index 999), which would otherwise sit on top of the default
|
||||
// tooltip (z-index 50). Bump the tooltip above the panel.
|
||||
.dockToggleTooltip {
|
||||
--tooltip-z-index: 1000;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
.span-details-panel {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
&__header-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
background: var(--l1-background);
|
||||
font-size: 14px;
|
||||
gap: 16px;
|
||||
|
||||
.data-viewer {
|
||||
min-height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
&__details-section {
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__tabs-section {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-height: 100%;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
// TabsRoot — direct child of tabs-section
|
||||
> div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[role='tablist'] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[class*='tabs__list-wrapper'] {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__tabs-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
[role='tabpanel'] {
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__span-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__span-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 16px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
&__span-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--l2-foreground);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__highlighted-options {
|
||||
padding: 8px 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0;
|
||||
|
||||
.key-value-label {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&__service-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-forest);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__trace-id {
|
||||
color: var(--accent-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__trace-id-copy {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&__key-attributes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
|
||||
&-label {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--l2-foreground);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.48px;
|
||||
line-height: var(--line-height-20);
|
||||
}
|
||||
|
||||
&-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip is rendered in a portal but the SpanDetailsPanel can be docked as a
|
||||
// FloatingPanel (z-index 999), which would otherwise sit on top of the default
|
||||
// tooltip (z-index 50). Bump the tooltip above the panel.
|
||||
.dock-toggle-tooltip {
|
||||
--tooltip-z-index: 1000;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
TabsTrigger,
|
||||
} from '@signozhq/ui/tabs';
|
||||
import {
|
||||
TooltipRoot,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
@@ -72,7 +72,7 @@ import SpanPercentileBadge from './SpanPercentile/SpanPercentileBadge';
|
||||
import SpanPercentilePanel from './SpanPercentile/SpanPercentilePanel';
|
||||
import useSpanPercentile from './SpanPercentile/useSpanPercentile';
|
||||
|
||||
import styles from './SpanDetailsPanel.module.scss';
|
||||
import './SpanDetailsPanel.styles.scss';
|
||||
|
||||
interface SpanDetailsPanelProps {
|
||||
panelState: DetailsPanelState;
|
||||
@@ -275,9 +275,9 @@ function SpanDetailsContent({
|
||||
// }, [selectedSpan]);
|
||||
|
||||
return (
|
||||
<div className={styles.body}>
|
||||
<div className={styles.detailsSection}>
|
||||
<div className={styles.spanRow}>
|
||||
<div className="span-details-panel__body">
|
||||
<div className="span-details-panel__details-section">
|
||||
<div className="span-details-panel__span-row">
|
||||
<KeyValueLabel
|
||||
badgeKey="Span name"
|
||||
badgeValue={selectedSpan.name}
|
||||
@@ -296,8 +296,8 @@ function SpanDetailsContent({
|
||||
<SpanPercentilePanel selectedSpan={selectedSpan} percentile={percentile} />
|
||||
|
||||
{/* Span info: exec time + start time */}
|
||||
<div className={styles.spanInfo}>
|
||||
<div className={styles.spanInfoItem}>
|
||||
<div className="span-details-panel__span-info">
|
||||
<div className="span-details-panel__span-info-item">
|
||||
<Timer size={14} />
|
||||
<span>
|
||||
{getYAxisFormattedValue(`${selectedSpan.duration_nano / 1000000}`, 'ms')}
|
||||
@@ -316,13 +316,13 @@ function SpanDetailsContent({
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.spanInfoItem}>
|
||||
<div className="span-details-panel__span-info-item">
|
||||
<CalendarClock size={14} />
|
||||
<span>
|
||||
{dayjs(selectedSpan.timestamp).format('HH:mm:ss — MMM D, YYYY')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.spanInfoItem}>
|
||||
<div className="span-details-panel__span-info-item">
|
||||
<Link2 size={14} />
|
||||
<LinkedSpansToggle
|
||||
count={linkedSpans.count}
|
||||
@@ -338,7 +338,7 @@ function SpanDetailsContent({
|
||||
/>
|
||||
|
||||
{/* Step 6: HighlightedOptions */}
|
||||
<div className={styles.highlightedOptions}>
|
||||
<div className="span-details-panel__highlighted-options">
|
||||
{HIGHLIGHTED_OPTIONS.map((option) => {
|
||||
const rendered = option.render(selectedSpan);
|
||||
if (!rendered) {
|
||||
@@ -382,7 +382,7 @@ function SpanDetailsContent({
|
||||
{/* Step 8: MiniTraceContext */}
|
||||
</div>
|
||||
|
||||
<div className={styles.tabsSection}>
|
||||
<div className="span-details-panel__tabs-section">
|
||||
{/* Step 9: ContentTabs */}
|
||||
<TabsRoot defaultValue="overview">
|
||||
<TabsList variant="secondary">
|
||||
@@ -402,7 +402,7 @@ function SpanDetailsContent({
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
<div className={styles.tabsScroll}>
|
||||
<div className="span-details-panel__tabs-scroll">
|
||||
<TabsContent value="overview">
|
||||
<DataViewer
|
||||
data={spanDisplayData}
|
||||
@@ -497,7 +497,7 @@ function SpanDetailsPanel({
|
||||
key: 'dock-toggle',
|
||||
component: (
|
||||
<TooltipProvider>
|
||||
<TooltipRoot>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -512,10 +512,10 @@ function SpanDetailsPanel({
|
||||
{isDocked ? <Dock size={14} /> : <PanelBottom size={14} />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={styles.dockToggleTooltip}>
|
||||
<TooltipContent className="dock-toggle-tooltip">
|
||||
{isDocked ? 'Open as floating panel' : 'Dock at the bottom'}
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
),
|
||||
});
|
||||
@@ -546,7 +546,7 @@ function SpanDetailsPanel({
|
||||
traceEndTime={traceEndTime}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.body}>
|
||||
<div className="span-details-panel__body">
|
||||
<Skeleton active paragraph={{ rows: 6 }} title={{ width: '60%' }} />
|
||||
</div>
|
||||
)}
|
||||
@@ -554,7 +554,7 @@ function SpanDetailsPanel({
|
||||
);
|
||||
|
||||
if (variant === SpanDetailVariant.DOCKED) {
|
||||
return <div className={styles.root}>{content}</div>;
|
||||
return <div className="span-details-panel">{content}</div>;
|
||||
}
|
||||
|
||||
if (variant === SpanDetailVariant.DRAWER) {
|
||||
@@ -562,7 +562,7 @@ function SpanDetailsPanel({
|
||||
<DetailsPanelDrawer
|
||||
isOpen={panelState.isOpen}
|
||||
onClose={panelState.close}
|
||||
className={styles.root}
|
||||
className="span-details-panel"
|
||||
>
|
||||
{content}
|
||||
</DetailsPanelDrawer>
|
||||
@@ -572,7 +572,7 @@ function SpanDetailsPanel({
|
||||
return (
|
||||
<FloatingPanel
|
||||
isOpen={panelState.isOpen}
|
||||
className={styles.root}
|
||||
className="span-details-panel"
|
||||
width={PANEL_WIDTH}
|
||||
minWidth={480}
|
||||
height={window.innerHeight - PANEL_MARGIN_TOP - PANEL_MARGIN_BOTTOM}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
// Badge — wraps a KeyValueLabel, clickable to toggle panel
|
||||
.span-percentile-badge {
|
||||
cursor: pointer;
|
||||
|
||||
// Override key color for the percentile value (p99)
|
||||
.key-value-label__key {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
&__loader {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 4px;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
&__value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
// Panel — collapsible, renders below the row
|
||||
.span-percentile-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 4px;
|
||||
filter: drop-shadow(2px 4px 16px rgba(0, 0, 0, 0.2));
|
||||
backdrop-filter: blur(20px);
|
||||
margin: 8px 16px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
&-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
|
||||
&-title {
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-20);
|
||||
}
|
||||
|
||||
&-highlight {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
&-loader {
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
margin: 0 4px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&__timerange {
|
||||
width: 100%;
|
||||
|
||||
&-select {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: 50px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__table {
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
&-text {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
&-rows {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&-skeleton {
|
||||
.ant-skeleton-title {
|
||||
width: 100% !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.ant-skeleton-paragraph {
|
||||
margin-top: 8px;
|
||||
|
||||
& > li + li {
|
||||
margin-top: 10px;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 4px;
|
||||
|
||||
&-key {
|
||||
flex: 0 0 auto;
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&-value {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&-dash {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
margin: 0 8px;
|
||||
border-top: 1px solid transparent;
|
||||
border-image: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--l1-border) 0,
|
||||
var(--l1-border) 10px,
|
||||
transparent 10px,
|
||||
transparent 20px
|
||||
)
|
||||
1 stretch;
|
||||
}
|
||||
|
||||
&--current {
|
||||
border-radius: 2px;
|
||||
background: rgba(78, 116, 248, 0.2);
|
||||
|
||||
.span-percentile-panel__table-row-key {
|
||||
color: var(--text-robin-300);
|
||||
}
|
||||
|
||||
.span-percentile-panel__table-row-dash {
|
||||
border-image: repeating-linear-gradient(
|
||||
to right,
|
||||
#abbdff 0,
|
||||
#abbdff 10px,
|
||||
transparent 10px,
|
||||
transparent 20px
|
||||
)
|
||||
1 stretch;
|
||||
}
|
||||
|
||||
.span-percentile-panel__table-row-value {
|
||||
color: var(--text-robin-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__resource-selector {
|
||||
overflow: hidden;
|
||||
width: calc(100% + 16px);
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
left: -8px;
|
||||
z-index: 1000;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
|
||||
&-header {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
&-input {
|
||||
border-radius: 0;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
&-items {
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.3rem;
|
||||
height: 0.3rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
|
||||
&-value {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Badge — wraps a KeyValueLabel, clickable to toggle panel
|
||||
.root {
|
||||
cursor: pointer;
|
||||
|
||||
// KeyValueLabel renders its key with a global class; recolor only the badge
|
||||
// instance inside this badge wrapper.
|
||||
:global(.key-value-label__key) {
|
||||
color: var(--destructive);
|
||||
}
|
||||
}
|
||||
|
||||
.loader {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 4px;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
||||
|
||||
import { UseSpanPercentileReturn } from './useSpanPercentile';
|
||||
|
||||
import styles from './SpanPercentileBadge.module.scss';
|
||||
import './SpanPercentile.styles.scss';
|
||||
|
||||
type SpanPercentileBadgeProps = Pick<
|
||||
UseSpanPercentileReturn,
|
||||
@@ -25,7 +25,7 @@ function SpanPercentileBadge({
|
||||
}: SpanPercentileBadgeProps): JSX.Element | null {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={styles.loader}>
|
||||
<div className="span-percentile-badge__loader">
|
||||
<Loader size={14} className="animate-spin" />
|
||||
</div>
|
||||
);
|
||||
@@ -37,7 +37,7 @@ function SpanPercentileBadge({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.root}
|
||||
className="span-percentile-badge"
|
||||
onClick={toggleOpen}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -50,12 +50,12 @@ function SpanPercentileBadge({
|
||||
<KeyValueLabel
|
||||
badgeKey={`p${percentileValue}`}
|
||||
badgeValue={
|
||||
<span className={styles.value}>
|
||||
<span className="span-percentile-badge__value">
|
||||
{duration}
|
||||
{isOpen ? (
|
||||
<ChevronUp size={14} className={styles.icon} />
|
||||
<ChevronUp size={14} className="span-percentile-badge__icon" />
|
||||
) : (
|
||||
<ChevronDown size={14} className={styles.icon} />
|
||||
<ChevronDown size={14} className="span-percentile-badge__icon" />
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
// Panel — collapsible, renders below the row
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 4px;
|
||||
filter: drop-shadow(2px 4px 16px rgba(0, 0, 0, 0.2));
|
||||
backdrop-filter: blur(20px);
|
||||
margin: 8px 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.contentTitle {
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-20);
|
||||
}
|
||||
|
||||
.contentHighlight {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
.contentLoader {
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
margin: 0 4px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.timerange {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.timerangeSelect {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
:global(.ant-select-selector) {
|
||||
border-radius: 50px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.tableHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tableHeaderText {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.tableRows {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tableSkeleton {
|
||||
:global(.ant-skeleton-title) {
|
||||
width: 100% !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
:global(.ant-skeleton-paragraph) {
|
||||
margin-top: 8px;
|
||||
|
||||
& > li + li {
|
||||
margin-top: 10px;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tableRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.tableRowKey {
|
||||
flex: 0 0 auto;
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.tableRowValue {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.tableRowDash {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
margin: 0 8px;
|
||||
border-top: 1px solid transparent;
|
||||
border-image: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--l1-border) 0,
|
||||
var(--l1-border) 10px,
|
||||
transparent 10px,
|
||||
transparent 20px
|
||||
)
|
||||
1 stretch;
|
||||
}
|
||||
|
||||
.isCurrent {
|
||||
border-radius: 2px;
|
||||
background: rgba(78, 116, 248, 0.2);
|
||||
|
||||
.tableRowKey {
|
||||
color: var(--text-robin-300);
|
||||
}
|
||||
|
||||
.tableRowDash {
|
||||
border-image: repeating-linear-gradient(
|
||||
to right,
|
||||
#abbdff 0,
|
||||
#abbdff 10px,
|
||||
transparent 10px,
|
||||
transparent 20px
|
||||
)
|
||||
1 stretch;
|
||||
}
|
||||
|
||||
.tableRowValue {
|
||||
color: var(--text-robin-400);
|
||||
}
|
||||
}
|
||||
|
||||
.resourceSelector {
|
||||
overflow: hidden;
|
||||
width: calc(100% + 16px);
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
left: -8px;
|
||||
z-index: 1000;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.resourceSelectorHeader {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.resourceSelectorInput {
|
||||
border-radius: 0;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.resourceSelectorItems {
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.3rem;
|
||||
height: 0.3rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
.resourceSelectorItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.resourceSelectorItemValue {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Checkbox, Input, Select, Skeleton } from 'antd';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -10,7 +8,7 @@ import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
|
||||
import { UseSpanPercentileReturn } from './useSpanPercentile';
|
||||
|
||||
import styles from './SpanPercentilePanel.module.scss';
|
||||
import './SpanPercentile.styles.scss';
|
||||
|
||||
const DEFAULT_RESOURCE_ATTRIBUTES = {
|
||||
serviceName: 'service.name',
|
||||
@@ -55,46 +53,46 @@ function SpanPercentilePanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.header}>
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
<div className="span-percentile-panel">
|
||||
<div className="span-percentile-panel__header">
|
||||
<Typography.Text
|
||||
className="span-percentile-panel__header-text"
|
||||
onClick={toggleOpen}
|
||||
prefix={<ChevronDown size={16} />}
|
||||
>
|
||||
Span Percentile
|
||||
</Button>
|
||||
<ChevronDown size={16} /> Span Percentile
|
||||
</Typography.Text>
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
onClick={(): void =>
|
||||
setShowResourceAttributesSelector(!showResourceAttributesSelector)
|
||||
}
|
||||
prefix={
|
||||
showResourceAttributesSelector ? <Check size={16} /> : <Plus size={16} />
|
||||
}
|
||||
/>
|
||||
{showResourceAttributesSelector ? (
|
||||
<Check
|
||||
size={16}
|
||||
className="cursor-pointer span-percentile-panel__header-icon"
|
||||
onClick={(): void => setShowResourceAttributesSelector(false)}
|
||||
/>
|
||||
) : (
|
||||
<Plus
|
||||
size={16}
|
||||
className="cursor-pointer span-percentile-panel__header-icon"
|
||||
onClick={(): void => setShowResourceAttributesSelector(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showResourceAttributesSelector && (
|
||||
<div
|
||||
className={styles.resourceSelector}
|
||||
className="span-percentile-panel__resource-selector"
|
||||
ref={resourceAttributesSelectorRef}
|
||||
>
|
||||
<div className={styles.resourceSelectorHeader}>
|
||||
<div className="span-percentile-panel__resource-selector-header">
|
||||
<Input
|
||||
placeholder="Search resource attributes"
|
||||
className={styles.resourceSelectorInput}
|
||||
className="span-percentile-panel__resource-selector-input"
|
||||
value={resourceAttributesSearchQuery}
|
||||
onChange={(e): void =>
|
||||
setResourceAttributesSearchQuery(e.target.value as string)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.resourceSelectorItems}>
|
||||
<div className="span-percentile-panel__resource-selector-items">
|
||||
{spanResourceAttributes
|
||||
.filter((attr) =>
|
||||
attr.key
|
||||
@@ -102,7 +100,10 @@ function SpanPercentilePanel({
|
||||
.includes(resourceAttributesSearchQuery.toLowerCase()),
|
||||
)
|
||||
.map((attr) => (
|
||||
<div className={styles.resourceSelectorItem} key={attr.key}>
|
||||
<div
|
||||
className="span-percentile-panel__resource-selector-item"
|
||||
key={attr.key}
|
||||
>
|
||||
<Checkbox
|
||||
checked={attr.isSelected}
|
||||
onChange={(e): void => {
|
||||
@@ -117,7 +118,9 @@ function SpanPercentilePanel({
|
||||
attr.key === DEFAULT_RESOURCE_ATTRIBUTES.name
|
||||
}
|
||||
>
|
||||
<div className={styles.resourceSelectorItemValue}>{attr.key}</div>
|
||||
<div className="span-percentile-panel__resource-selector-item-value">
|
||||
{attr.key}
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
@@ -125,15 +128,15 @@ function SpanPercentilePanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.content}>
|
||||
<Typography.Text className={styles.contentTitle}>
|
||||
<div className="span-percentile-panel__content">
|
||||
<Typography.Text className="span-percentile-panel__content-title">
|
||||
This span duration is{' '}
|
||||
{!loading && spanPercentileData ? (
|
||||
<span className={styles.contentHighlight}>
|
||||
<span className="span-percentile-panel__content-highlight">
|
||||
p{Math.floor(spanPercentileData.percentile || 0)}
|
||||
</span>
|
||||
) : (
|
||||
<span className={styles.contentLoader}>
|
||||
<span className="span-percentile-panel__content-loader">
|
||||
<Loader size={12} className="animate-spin" />
|
||||
</span>
|
||||
)}{' '}
|
||||
@@ -141,11 +144,11 @@ function SpanPercentilePanel({
|
||||
hour(s) since the span start time.
|
||||
</Typography.Text>
|
||||
|
||||
<div className={styles.timerange}>
|
||||
<div className="span-percentile-panel__timerange">
|
||||
<Select
|
||||
labelInValue
|
||||
placeholder="Select timerange"
|
||||
className={styles.timerangeSelect}
|
||||
className="span-percentile-panel__timerange-select"
|
||||
getPopupContainer={(trigger): HTMLElement =>
|
||||
trigger.parentElement || document.body
|
||||
}
|
||||
@@ -164,45 +167,45 @@ function SpanPercentilePanel({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={styles.tableHeader}>
|
||||
<Typography.Text className={styles.tableHeaderText}>
|
||||
<div className="span-percentile-panel__table">
|
||||
<div className="span-percentile-panel__table-header">
|
||||
<Typography.Text className="span-percentile-panel__table-header-text">
|
||||
Percentile
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.tableHeaderText}>
|
||||
<Typography.Text className="span-percentile-panel__table-header-text">
|
||||
Duration
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className={styles.tableRows}>
|
||||
<div className="span-percentile-panel__table-rows">
|
||||
{isLoadingData || isFetchingData ? (
|
||||
<Skeleton
|
||||
active
|
||||
paragraph={{ rows: 3 }}
|
||||
className={styles.tableSkeleton}
|
||||
className="span-percentile-panel__table-skeleton"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{Object.entries(spanPercentileData?.percentiles || {}).map(
|
||||
([pKey, pDuration]) => (
|
||||
<div className={styles.tableRow} key={pKey}>
|
||||
<Typography.Text className={styles.tableRowKey}>
|
||||
<div className="span-percentile-panel__table-row" key={pKey}>
|
||||
<Typography.Text className="span-percentile-panel__table-row-key">
|
||||
{pKey}
|
||||
</Typography.Text>
|
||||
<div className={styles.tableRowDash} />
|
||||
<Typography.Text className={styles.tableRowValue}>
|
||||
<div className="span-percentile-panel__table-row-dash" />
|
||||
<Typography.Text className="span-percentile-panel__table-row-value">
|
||||
{getYAxisFormattedValue(`${pDuration / 1000000}`, 'ms')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
|
||||
<div className={cx(styles.tableRow, styles.isCurrent)}>
|
||||
<Typography.Text className={styles.tableRowKey}>
|
||||
<div className="span-percentile-panel__table-row span-percentile-panel__table-row--current">
|
||||
<Typography.Text className="span-percentile-panel__table-row-key">
|
||||
p{Math.floor(spanPercentileData?.percentile || 0)}
|
||||
</Typography.Text>
|
||||
<div className={styles.tableRowDash} />
|
||||
<Typography.Text className={styles.tableRowValue}>
|
||||
<div className="span-percentile-panel__table-row-dash" />
|
||||
<Typography.Text className="span-percentile-panel__table-row-value">
|
||||
(this span){' '}
|
||||
{getYAxisFormattedValue(
|
||||
`${selectedSpan.duration_nano / 1000000}`,
|
||||
|
||||
@@ -5,8 +5,6 @@ import { toast } from '@signozhq/ui/sonner';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
|
||||
import styles from './SpanDetailsPanel.module.scss';
|
||||
|
||||
interface TraceIdFieldProps {
|
||||
span: SpanV3;
|
||||
}
|
||||
@@ -38,7 +36,7 @@ export function TraceIdField({ span }: TraceIdFieldProps): JSX.Element {
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
className={styles.traceIdCopy}
|
||||
className="span-details-panel__trace-id-copy"
|
||||
onClick={handleCopy}
|
||||
title="Click to copy trace ID"
|
||||
>
|
||||
@@ -53,7 +51,7 @@ export function TraceIdField({ span }: TraceIdFieldProps): JSX.Element {
|
||||
pathname: `/trace/${span.trace_id}`,
|
||||
search: window.location.search,
|
||||
}}
|
||||
className={styles.traceId}
|
||||
className="span-details-panel__trace-id"
|
||||
>
|
||||
{span.trace_id}
|
||||
</Link>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ReactNode } from 'react';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
|
||||
import styles from './SpanDetailsPanel.module.scss';
|
||||
import { TraceIdField } from './TraceIdField';
|
||||
|
||||
interface HighlightedOption {
|
||||
@@ -18,7 +17,7 @@ export const HIGHLIGHTED_OPTIONS: HighlightedOption[] = [
|
||||
render: (span): ReactNode | null =>
|
||||
span['service.name'] ? (
|
||||
<Badge color="vanilla">
|
||||
<span className={styles.serviceDot} />
|
||||
<span className="span-details-panel__service-dot" />
|
||||
{span['service.name']}
|
||||
</Badge>
|
||||
) : null,
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
.root {
|
||||
font-size: 12px;
|
||||
color: var(--l1-foreground);
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--l2-foreground);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
color: var(--text-robin-400);
|
||||
}
|
||||
|
||||
.hasError {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 11px;
|
||||
color: var(--l2-foreground);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: 1px solid var(--l2-border);
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.attributes {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.kv {
|
||||
margin-bottom: 2px;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.key {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
.event-tooltip-content {
|
||||
font-size: 12px;
|
||||
color: var(--l1-foreground);
|
||||
max-width: 300px;
|
||||
|
||||
&__header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--l2-foreground);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
color: var(--text-robin-400);
|
||||
|
||||
&.error {
|
||||
color: var(--destructive);
|
||||
}
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: 11px;
|
||||
color: var(--l2-foreground);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&__divider {
|
||||
border-top: 1px solid var(--l2-border);
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
&__attributes {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
&__kv {
|
||||
margin-bottom: 2px;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&__key {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
&__value {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
||||
import { Diamond } from '@signozhq/icons';
|
||||
import cx from 'classnames';
|
||||
import { toFixed } from 'utils/toFixed';
|
||||
|
||||
import styles from './EventTooltipContent.module.scss';
|
||||
import './EventTooltipContent.styles.scss';
|
||||
|
||||
export interface EventTooltipContentProps {
|
||||
eventName: string;
|
||||
@@ -21,25 +20,25 @@ export function EventTooltipContent({
|
||||
const { time, timeUnitName } = convertTimeToRelevantUnit(timeOffsetMs);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.header}>
|
||||
<div className="event-tooltip-content">
|
||||
<div className="event-tooltip-content__header">
|
||||
<Diamond size={10} />
|
||||
<span>EVENT DETAILS</span>
|
||||
</div>
|
||||
<div className={cx(styles.name, isError && styles.hasError)}>
|
||||
<div className={`event-tooltip-content__name ${isError ? 'error' : ''}`}>
|
||||
{eventName}
|
||||
</div>
|
||||
<div className={styles.time}>
|
||||
<div className="event-tooltip-content__time">
|
||||
{toFixed(time, 2)} {timeUnitName} since span start
|
||||
</div>
|
||||
{Object.keys(attributeMap).length > 0 && (
|
||||
<>
|
||||
<div className={styles.divider} />
|
||||
<div className={styles.attributes}>
|
||||
<div className="event-tooltip-content__divider" />
|
||||
<div className="event-tooltip-content__attributes">
|
||||
{Object.entries(attributeMap).map(([key, value]) => (
|
||||
<div key={key} className={styles.kv}>
|
||||
<span className={styles.key}>{key}:</span>{' '}
|
||||
<span className={styles.value}>{value}</span>
|
||||
<div key={key} className="event-tooltip-content__kv">
|
||||
<span className="event-tooltip-content__key">{key}:</span>{' '}
|
||||
<span className="event-tooltip-content__value">{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
// `top` is updated by the parent to track the hovered row's Y; its `left`
|
||||
// is the sidebar/timeline boundary so the popover always opens at the same
|
||||
// X regardless of which row is hovered.
|
||||
.anchor {
|
||||
.span-hover-card-anchor {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.popover {
|
||||
.span-hover-card-popover {
|
||||
// Hover card may be rendered while the SpanDetailsPanel is docked as
|
||||
// a FloatingPanel (z-index 999); bump above the default tooltip z-index.
|
||||
--tooltip-z-index: 1000;
|
||||
@@ -20,29 +20,31 @@
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.content {
|
||||
// Flamegraph tooltip — rendered as a portal, uses same semantic tokens.
|
||||
// Position is set inline on the element (left/top track the cursor); the
|
||||
// static layout/decoration lives here.
|
||||
.flamegraph-tooltip {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
background-color: var(--l1-background);
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
.span-hover-card-content {
|
||||
font-size: 12px;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
&__name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.row {
|
||||
line-height: 1.5;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.preview {
|
||||
// container for additional preview rows
|
||||
}
|
||||
|
||||
.previewKey {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.previewValue {
|
||||
color: var(--l1-foreground);
|
||||
&__row {
|
||||
line-height: 1.5;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
TooltipRoot,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
@@ -11,7 +11,7 @@ import { useMemo } from 'react';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
import { toFixed } from 'utils/toFixed';
|
||||
|
||||
import styles from './SpanHoverCard.module.scss';
|
||||
import './SpanHoverCard.styles.scss';
|
||||
|
||||
/**
|
||||
* Span-level fields that the tooltip always shows (as the colored title or
|
||||
@@ -51,21 +51,27 @@ export function SpanTooltipContent({
|
||||
convertTimeToRelevantUnit(durationMs);
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.name} style={{ color }}>
|
||||
<div className="span-hover-card-content">
|
||||
<div className="span-hover-card-content__name" style={{ color }}>
|
||||
{spanName}
|
||||
</div>
|
||||
<div className={styles.row}>status: {hasError ? 'error' : 'ok'}</div>
|
||||
<div className={styles.row}>start: {toFixed(relativeStartMs, 2)} ms</div>
|
||||
<div className={styles.row}>
|
||||
<div className="span-hover-card-content__row">
|
||||
status: {hasError ? 'error' : 'ok'}
|
||||
</div>
|
||||
<div className="span-hover-card-content__row">
|
||||
start: {toFixed(relativeStartMs, 2)} ms
|
||||
</div>
|
||||
<div className="span-hover-card-content__row">
|
||||
duration: {toFixed(formattedDuration, 2)} {timeUnitName}
|
||||
</div>
|
||||
{previewRows && previewRows.length > 0 && (
|
||||
<div className={styles.preview}>
|
||||
<div className="span-hover-card-content__preview">
|
||||
{previewRows.map((row) => (
|
||||
<div key={row.key} className={styles.row}>
|
||||
<span className={styles.previewKey}>{row.key}:</span>{' '}
|
||||
<span className={styles.previewValue}>{row.value}</span>
|
||||
<div key={row.key} className="span-hover-card-content__row">
|
||||
<span className="span-hover-card-content__preview-key">{row.key}:</span>{' '}
|
||||
<span className="span-hover-card-content__preview-value">
|
||||
{row.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -143,10 +149,10 @@ export function SpanHoverCard({
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipRoot open={hoverCardData !== null} onOpenChange={onOpenChange}>
|
||||
<Tooltip open={hoverCardData !== null} onOpenChange={onOpenChange}>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={styles.anchor}
|
||||
className="span-hover-card-anchor"
|
||||
style={{
|
||||
top: hoverCardData?.anchorTop ?? 0,
|
||||
left: anchorLeft,
|
||||
@@ -158,11 +164,11 @@ export function SpanHoverCard({
|
||||
side="right"
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
className={styles.popover}
|
||||
className="span-hover-card-popover"
|
||||
>
|
||||
{hoverCardData && <SpanTooltipContent {...hoverCardData.tooltip} />}
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
.wrapper {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
gap: 8px;
|
||||
|
||||
// KeyValueLabel renders with a global `.key-value-label` root; keep it from
|
||||
// shrinking on the trace details header.
|
||||
:global(.key-value-label) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.backBtn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filter {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.isExpanded {
|
||||
max-width: none;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.oldViewBtn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.analyticsBtn {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.subHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 4px 16px 8px;
|
||||
font-size: 13px;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.subItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: var(--l2-foreground);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.entryPointBadge {
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
:global(.ant-skeleton-input) {
|
||||
width: 160px !important;
|
||||
min-height: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
.trace-details-header-wrapper {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.trace-details-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
gap: 8px;
|
||||
|
||||
&__back-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.key-value-label {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__filter {
|
||||
&.trace-v3-filter-row {
|
||||
padding: 0;
|
||||
}
|
||||
min-width: 0;
|
||||
|
||||
&--expanded {
|
||||
max-width: none;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__old-view-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__analytics-btn {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&__sub-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 4px 16px 8px;
|
||||
font-size: 13px;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
&__sub-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__separator {
|
||||
color: var(--l2-foreground);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&__entry-point-badge {
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__skeleton .ant-skeleton-input {
|
||||
width: 160px !important;
|
||||
min-height: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,13 @@ import { useCallback, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import {
|
||||
TooltipRoot,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@signozhq/ui/tooltip';
|
||||
import { Skeleton } from 'antd';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import cx from 'classnames';
|
||||
import HttpStatusBadge from 'components/HttpStatusBadge/HttpStatusBadge';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -34,7 +33,7 @@ import AnalyticsPanel from '../SpanDetailsPanel/AnalyticsPanel/AnalyticsPanel';
|
||||
import Filters from '../TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters';
|
||||
import TraceOptionsMenu from './TraceOptionsMenu';
|
||||
|
||||
import styles from './TraceDetailsHeader.module.scss';
|
||||
import './TraceDetailsHeader.styles.scss';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
|
||||
interface FilterMetadata {
|
||||
@@ -69,7 +68,7 @@ function DetailsLoader(): JSX.Element {
|
||||
key={i}
|
||||
active
|
||||
size="small"
|
||||
className={styles.skeleton}
|
||||
className="trace-details-header__skeleton"
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
@@ -121,15 +120,15 @@ function TraceDetailsHeader({
|
||||
convertTimeToRelevantUnit(durationMs);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.header}>
|
||||
<div className="trace-details-header-wrapper">
|
||||
<div className="trace-details-header">
|
||||
{!isFilterExpanded && (
|
||||
<>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="md"
|
||||
className={styles.backBtn}
|
||||
className="trace-details-header__back-btn"
|
||||
onClick={handlePreviousBtnClick}
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
@@ -146,20 +145,20 @@ function TraceDetailsHeader({
|
||||
{!isFilterExpanded && (
|
||||
<>
|
||||
<TooltipProvider>
|
||||
<TooltipRoot>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
className={styles.analyticsBtn}
|
||||
className="trace-details-header__analytics-btn"
|
||||
onClick={(): void => setIsAnalyticsOpen((prev) => !prev)}
|
||||
>
|
||||
<ChartPie size={14} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Analytics</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TraceOptionsMenu
|
||||
showTraceDetails={showTraceDetails}
|
||||
@@ -168,7 +167,11 @@ function TraceDetailsHeader({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className={cx(styles.filter, isFilterExpanded && styles.isExpanded)}>
|
||||
<div
|
||||
className={`trace-details-header__filter${
|
||||
isFilterExpanded ? ' trace-details-header__filter--expanded' : ''
|
||||
}`}
|
||||
>
|
||||
<Filters
|
||||
startTime={filterMetadata.startTime}
|
||||
endTime={filterMetadata.endTime}
|
||||
@@ -184,7 +187,7 @@ function TraceDetailsHeader({
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
className={styles.oldViewBtn}
|
||||
className="trace-details-header__old-view-btn"
|
||||
onClick={handleSwitchToOldView}
|
||||
>
|
||||
Legacy View
|
||||
@@ -195,22 +198,22 @@ function TraceDetailsHeader({
|
||||
</div>
|
||||
|
||||
{showTraceDetails && (
|
||||
<div className={styles.subHeader}>
|
||||
<div className="trace-details-header__sub-header">
|
||||
{traceMetadata ? (
|
||||
<>
|
||||
<span className={styles.subItem}>
|
||||
<span className="trace-details-header__sub-item">
|
||||
<Server size={13} />
|
||||
{traceMetadata.rootServiceName}
|
||||
<span className={styles.separator}>—</span>
|
||||
<span className={styles.entryPointBadge}>
|
||||
<span className="trace-details-header__separator">—</span>
|
||||
<span className="trace-details-header__entry-point-badge">
|
||||
{traceMetadata.rootServiceEntryPoint}
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.subItem}>
|
||||
<span className="trace-details-header__sub-item">
|
||||
<Timer size={13} />
|
||||
{parseFloat(formattedDuration.toFixed(2))} {timeUnitName}
|
||||
</span>
|
||||
<span className={styles.subItem}>
|
||||
<span className="trace-details-header__sub-item">
|
||||
<CalendarClock size={13} />
|
||||
{dayjs(traceMetadata.startTimestampMillis).format(
|
||||
DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS,
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
.root {
|
||||
height: calc(100vh);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Shared Ant Collapse chrome reset for both the flamegraph and waterfall
|
||||
// collapse panels.
|
||||
.flameCollapse,
|
||||
.waterfallCollapse {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
|
||||
:global(.ant-collapse-item) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
:global(.ant-collapse-header) {
|
||||
border-top: 1px solid var(--l2-border);
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
:global(.ant-collapse-content) {
|
||||
background: transparent;
|
||||
border-top: none;
|
||||
// Disable collapse animation — virtualizer and canvas flicker during
|
||||
// height transitions.
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.collapseLabel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.collapseTitle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.collapseCount {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.collapseCountItem {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.hasErrors {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
.flameCollapse {
|
||||
flex-shrink: 0;
|
||||
|
||||
:global(.ant-collapse-content-box) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.waterfallCollapse {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
:global(.ant-collapse-item) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:global(.ant-collapse-content.ant-collapse-content-active) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:global(.ant-collapse-content-box) {
|
||||
padding: 0 !important;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
&.isDocked {
|
||||
flex: none;
|
||||
|
||||
:global(.ant-collapse-item) {
|
||||
flex: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
:global(.ant-collapse-content-box) {
|
||||
flex: none;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dockedSpanDetails {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
124
frontend/src/pages/TraceDetailsV3/TraceDetailsV3.styles.scss
Normal file
124
frontend/src/pages/TraceDetailsV3/TraceDetailsV3.styles.scss
Normal file
@@ -0,0 +1,124 @@
|
||||
.trace-details-v3 {
|
||||
height: calc(100vh);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__flame-collapse,
|
||||
&__waterfall-collapse {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
|
||||
.ant-collapse-item {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ant-collapse-header {
|
||||
border-top: 1px solid var(--l2-border);
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
background: transparent;
|
||||
border-top: none;
|
||||
// Disable collapse animation — virtualizer and canvas flicker during height transitions
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__collapse-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__collapse-count {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
&__collapse-count-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__collapse-count-errors {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
&__flame-collapse {
|
||||
flex-shrink: 0;
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__waterfall-collapse {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-collapse-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ant-collapse-content.ant-collapse-content-active {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 0 !important;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
&--docked {
|
||||
flex: none;
|
||||
|
||||
.ant-collapse-item {
|
||||
flex: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
flex: none;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__docked-span-details {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.viewport {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: block;
|
||||
width: 100%;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Flamegraph tooltip — rendered as a portal, follows the cursor.
|
||||
// Position is set inline on the element (left/top); the layout/decoration
|
||||
// lives here. Shares visual treatment with the waterfall hover popover
|
||||
// but is positioned `fixed` instead of anchored.
|
||||
.tooltip {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
background-color: var(--l1-background);
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--l2-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
.flamegraph-canvas {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.flamegraph-canvas__viewport {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flamegraph-canvas__main {
|
||||
display: block;
|
||||
width: 100%;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.flamegraph-canvas__overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import { useFlamegraphZoom } from './hooks/useFlamegraphZoom';
|
||||
import { useScrollToSpan } from './hooks/useScrollToSpan';
|
||||
import { EventRect, FlamegraphCanvasProps, SpanRect } from './types';
|
||||
|
||||
import styles from './FlamegraphCanvas.module.scss';
|
||||
import './FlamegraphCanvas.styles.scss';
|
||||
|
||||
function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
|
||||
const {
|
||||
@@ -194,7 +194,7 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
|
||||
const tooltipElement = tooltipContent
|
||||
? createPortal(
|
||||
<div
|
||||
className={styles.tooltip}
|
||||
className="span-hover-card-popover flamegraph-tooltip"
|
||||
style={{
|
||||
left: Math.min(tooltipContent.clientX + 15, window.innerWidth - 220),
|
||||
top: Math.min(tooltipContent.clientY + 15, window.innerHeight - 100),
|
||||
@@ -223,7 +223,7 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className="flamegraph-canvas">
|
||||
{tooltipElement}
|
||||
<TimelineV3
|
||||
startTimestamp={viewStartTs}
|
||||
@@ -234,7 +234,7 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
|
||||
/>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={styles.viewport}
|
||||
className="flamegraph-canvas__viewport"
|
||||
onMouseEnter={(): void => {
|
||||
isOverFlamegraphRef.current = true;
|
||||
}}
|
||||
@@ -242,7 +242,7 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={styles.main}
|
||||
className="flamegraph-canvas__main"
|
||||
onMouseDown={(e): void => {
|
||||
handleMouseDown(e);
|
||||
handleMouseDownForClick(e);
|
||||
@@ -251,7 +251,7 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
|
||||
onMouseUp={handleMouseUp}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<canvas ref={overlayCanvasRef} className={styles.overlay} />
|
||||
<canvas ref={overlayCanvasRef} className="flamegraph-canvas__overlay" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
// Container — applied to the `.ant-modal` element via SignozModal's className
|
||||
// prop. Ant Modal portals into document.body, but the className still lives on
|
||||
// the modal root, so descendant overrides work via `:global` nesting.
|
||||
.container {
|
||||
:global(.ant-modal-content),
|
||||
:global(.ant-modal-header) {
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
:global(.ant-modal-header) {
|
||||
border-bottom: none;
|
||||
|
||||
:global(.ant-modal-title) {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-modal-body) {
|
||||
padding: 14px 16px !important;
|
||||
padding-bottom: 0 !important;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
:global(.ant-modal-footer) {
|
||||
margin-top: 0;
|
||||
background: var(--l2-background);
|
||||
border-top: 1px solid var(--l2-border);
|
||||
padding: 16px !important;
|
||||
|
||||
.saveButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
width: 135px;
|
||||
|
||||
:global(.ant-btn-icon) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: var(--l2-foreground);
|
||||
|
||||
:global(.ant-btn-icon svg) {
|
||||
stroke: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.discardButton {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
:global(.ant-btn) {
|
||||
border-radius: 2px;
|
||||
padding: 4px 8px;
|
||||
margin: 0 !important;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inner content wrapper (sibling of modal chrome)
|
||||
.root {
|
||||
// Funnel-detail view overrides — only when the inner wrapper has
|
||||
// `.isDetails` applied alongside `.root`.
|
||||
&.isDetails {
|
||||
:global(.traces-funnel-details) {
|
||||
height: unset;
|
||||
|
||||
:global(.traces-funnel-details__steps-config) {
|
||||
width: unset;
|
||||
border: none;
|
||||
}
|
||||
|
||||
:global(.funnel-step-wrapper) {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
:global(.steps-content) {
|
||||
max-height: 500px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.funnel-item) {
|
||||
padding: 8px 8px 12px 16px;
|
||||
|
||||
&,
|
||||
&:first-child {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
:global(.funnel-item__header) {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
:global(.funnel-item__details) {
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loadingSpinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
background: var(--l3-background);
|
||||
|
||||
:global(.ant-input-prefix) {
|
||||
height: 18px;
|
||||
margin-inline-end: 6px;
|
||||
|
||||
svg {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&,
|
||||
input {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
font-weight: 400;
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: var(--l2-foreground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.createButton {
|
||||
width: 153px;
|
||||
padding: 4px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
background: var(--l3-background);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.list {
|
||||
max-height: 400px;
|
||||
overflow-y: scroll;
|
||||
|
||||
:global(.funnels-empty__content) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.funnels-list) {
|
||||
gap: 8px;
|
||||
|
||||
:global(.funnel-item) {
|
||||
padding: 8px 16px 12px;
|
||||
|
||||
:global(.funnel-item__details) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.backButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.detailsView {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
:global(.funnel-configuration__steps) {
|
||||
padding: 0;
|
||||
|
||||
:global(.funnel-step__content .filters__service-and-span .ant-select) {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
:global(.funnel-step__footer .error) {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
:global(.inter-step-config) {
|
||||
width: calc(100% - 104px);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.funnel-item__actions-popover) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
// Modal base styles
|
||||
.add-span-to-funnel-modal {
|
||||
&__loading-spinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
}
|
||||
&-container {
|
||||
.ant-modal {
|
||||
&-content,
|
||||
&-header {
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
&-header {
|
||||
border-bottom: none;
|
||||
|
||||
.ant-modal-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&-body {
|
||||
padding: 14px 16px !important;
|
||||
padding-bottom: 0 !important;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
margin-top: 0;
|
||||
background: var(--l2-background);
|
||||
border-top: 1px solid var(--l2-border);
|
||||
padding: 16px !important;
|
||||
.add-span-to-funnel-modal {
|
||||
&__save-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
width: 135px;
|
||||
|
||||
.ant-btn-icon {
|
||||
display: flex;
|
||||
}
|
||||
&:disabled {
|
||||
color: var(--l2-foreground);
|
||||
.ant-btn-icon {
|
||||
svg {
|
||||
stroke: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&__discard-button {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
.ant-btn {
|
||||
border-radius: 2px;
|
||||
padding: 4px 8px;
|
||||
margin: 0 !important;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main modal styles
|
||||
.add-span-to-funnel-modal {
|
||||
// Common button styles
|
||||
%button-base {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Details view styles
|
||||
&--details {
|
||||
.traces-funnel-details {
|
||||
height: unset;
|
||||
|
||||
&__steps-config {
|
||||
width: unset;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.funnel-step-wrapper {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.steps-content {
|
||||
max-height: 500px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search section
|
||||
&__search {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
align-items: center;
|
||||
|
||||
&-input {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
background: var(--l3-background);
|
||||
|
||||
.ant-input-prefix {
|
||||
height: 18px;
|
||||
margin-inline-end: 6px;
|
||||
svg {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&,
|
||||
input {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
font-weight: 400;
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: var(--l2-foreground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create button
|
||||
&__create-button {
|
||||
@extend %button-base;
|
||||
width: 153px;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
background: var(--l3-background);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
.funnel-item {
|
||||
padding: 8px 8px 12px 16px;
|
||||
&,
|
||||
&:first-child {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
line-height: 20px;
|
||||
}
|
||||
&__details {
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
// List section
|
||||
&__list {
|
||||
max-height: 400px;
|
||||
overflow-y: scroll;
|
||||
.funnels-empty {
|
||||
&__content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.funnels-list {
|
||||
gap: 8px;
|
||||
|
||||
.funnel-item {
|
||||
padding: 8px 16px 12px;
|
||||
|
||||
&__details {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__spinner {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
// Back button
|
||||
&__back-button {
|
||||
@extend %button-base;
|
||||
gap: 6px;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
// Details section
|
||||
&__details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
.funnel-configuration__steps {
|
||||
padding: 0;
|
||||
|
||||
.funnel-step {
|
||||
&__content .filters__service-and-span .ant-select {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
&__footer .error {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.inter-step-config {
|
||||
width: calc(100% - 104px);
|
||||
}
|
||||
}
|
||||
.funnel-item__actions-popover {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import { filterFunnelsByQuery } from 'pages/TracesFunnels/utils';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
import { FunnelData } from 'types/api/traceFunnels';
|
||||
|
||||
import styles from './AddSpanToFunnelModal.module.scss';
|
||||
import './AddSpanToFunnelModal.styles.scss';
|
||||
|
||||
enum ModalView {
|
||||
LIST = 'list',
|
||||
@@ -62,7 +62,7 @@ function FunnelDetailsView({
|
||||
}, [triggerDiscard, funnel.steps, handleRestoreSteps]);
|
||||
|
||||
return (
|
||||
<div className={styles.detailsView}>
|
||||
<div className="add-span-to-funnel-modal__details">
|
||||
<FunnelListItem
|
||||
funnel={funnel}
|
||||
shouldRedirectToTracesListOnDeleteSuccess={false}
|
||||
@@ -161,11 +161,11 @@ function AddSpanToFunnelModal({
|
||||
};
|
||||
|
||||
const renderListView = (): JSX.Element => (
|
||||
<div className={styles.root}>
|
||||
<div className="add-span-to-funnel-modal">
|
||||
{!!filteredData?.length && (
|
||||
<div className={styles.search}>
|
||||
<div className="add-span-to-funnel-modal__search">
|
||||
<Input
|
||||
className={styles.searchInput}
|
||||
className="add-span-to-funnel-modal__search-input"
|
||||
placeholder="Search by name, description, or tags..."
|
||||
prefix={<Search size={12} />}
|
||||
value={searchQuery}
|
||||
@@ -173,7 +173,7 @@ function AddSpanToFunnelModal({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.list}>
|
||||
<div className="add-span-to-funnel-modal__list">
|
||||
<OverlayScrollbar>
|
||||
<TracesFunnelsContentRenderer
|
||||
isError={isError}
|
||||
@@ -201,11 +201,11 @@ function AddSpanToFunnelModal({
|
||||
);
|
||||
|
||||
const renderDetailsView = ({ span }: { span: SpanV3 }): JSX.Element => (
|
||||
<div className={cx(styles.root, styles.isDetails)}>
|
||||
<div className="add-span-to-funnel-modal add-span-to-funnel-modal--details">
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className={styles.backButton}
|
||||
className="add-span-to-funnel-modal__back-button"
|
||||
onClick={handleBack}
|
||||
prefix={<ArrowLeft size={14} />}
|
||||
>
|
||||
@@ -214,7 +214,7 @@ function AddSpanToFunnelModal({
|
||||
<div className="traces-funnel-details">
|
||||
<div className="traces-funnel-details__steps-config">
|
||||
<Spin
|
||||
className={styles.loadingSpinner}
|
||||
className="add-span-to-funnel-modal__loading-spinner"
|
||||
spinning={isFunnelDetailsLoading || isFunnelDetailsFetching}
|
||||
indicator={<LoaderCircle size={14} className="animate-spin" />}
|
||||
>
|
||||
@@ -245,7 +245,10 @@ function AddSpanToFunnelModal({
|
||||
onCancel={onClose}
|
||||
width={570}
|
||||
title="Add span to funnel"
|
||||
className={styles.container}
|
||||
className={cx('add-span-to-funnel-modal-container', {
|
||||
'add-span-to-funnel-modal-container--details':
|
||||
activeView === ModalView.DETAILS,
|
||||
})}
|
||||
footer={
|
||||
activeView === ModalView.DETAILS
|
||||
? [
|
||||
@@ -254,7 +257,7 @@ function AddSpanToFunnelModal({
|
||||
color="secondary"
|
||||
key="discard"
|
||||
onClick={handleDiscard}
|
||||
className={styles.discardButton}
|
||||
className="add-span-to-funnel-modal__discard-button"
|
||||
disabled={!isUnsavedChanges}
|
||||
>
|
||||
Discard
|
||||
@@ -263,7 +266,7 @@ function AddSpanToFunnelModal({
|
||||
key="save"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
className={styles.saveButton}
|
||||
className="add-span-to-funnel-modal__save-button"
|
||||
onClick={handleSaveFunnel}
|
||||
disabled={!isUnsavedChanges}
|
||||
prefix={<Check size={14} />}
|
||||
@@ -276,7 +279,7 @@ function AddSpanToFunnelModal({
|
||||
key="create"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
className={styles.createButton}
|
||||
className="add-span-to-funnel-modal__create-button"
|
||||
onClick={handleCreateNewClick}
|
||||
prefix={<Plus size={14} />}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.root {
|
||||
.span-line-action-buttons {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
@@ -8,20 +8,20 @@
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
.copyBtn {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 9px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
border-color: var(--l1-border) !important;
|
||||
.copy-span-btn {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 9px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
border-color: var(--l1-border) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip rendered in a portal; bump above FloatingPanel (z-index 999) so it
|
||||
// stays visible when the SpanDetailsPanel is docked as a floating panel.
|
||||
.tooltip {
|
||||
.span-line-action-tooltip {
|
||||
--tooltip-z-index: 1000;
|
||||
}
|
||||
@@ -70,6 +70,24 @@ describe('SpanLineActionButtons', () => {
|
||||
expect(mockOnSpanCopy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('applies correct styling classes', () => {
|
||||
(useCopySpanLink as jest.Mock).mockReturnValue({
|
||||
onSpanCopy: jest.fn(),
|
||||
});
|
||||
|
||||
render(<SpanLineActionButtons span={mockSpan} />);
|
||||
|
||||
// Check if the main container has the correct class
|
||||
const container = screen
|
||||
.getByRole('button')
|
||||
.closest('.span-line-action-buttons');
|
||||
expect(container).toHaveClass('span-line-action-buttons');
|
||||
|
||||
// Check if the button has the correct class
|
||||
const copyButton = screen.getByRole('button');
|
||||
expect(copyButton).toHaveClass('copy-span-btn');
|
||||
});
|
||||
|
||||
it('copies span link to clipboard when copy button is clicked', () => {
|
||||
const mockSetCopy = jest.fn();
|
||||
const mockUrlQuery = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import {
|
||||
TooltipRoot,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
@@ -9,7 +9,7 @@ import { useCopySpanLink } from 'hooks/trace/useCopySpanLink';
|
||||
import { Link } from '@signozhq/icons';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
|
||||
import styles from './SpanLineActionButtons.module.scss';
|
||||
import './SpanLineActionButtons.styles.scss';
|
||||
|
||||
export interface SpanLineActionButtonsProps {
|
||||
span: Span;
|
||||
@@ -20,22 +20,24 @@ export default function SpanLineActionButtons({
|
||||
const { onSpanCopy } = useCopySpanLink(span);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className="span-line-action-buttons">
|
||||
<TooltipProvider>
|
||||
<TooltipRoot>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={onSpanCopy}
|
||||
className={styles.copyBtn}
|
||||
className="copy-span-btn"
|
||||
>
|
||||
<Link size={14} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={styles.tooltip}>Copy Span Link</TooltipContent>
|
||||
</TooltipRoot>
|
||||
<TooltipContent className="span-line-action-tooltip">
|
||||
Copy Span Link
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.root {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.loadingSkeleton {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.trace-waterfall {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.loading-skeleton {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { getVisibleSpans } from './utils';
|
||||
|
||||
import { IInterestedSpan } from './types';
|
||||
|
||||
import styles from './TraceWaterfall.module.scss';
|
||||
import './TraceWaterfall.styles.scss';
|
||||
|
||||
interface ITraceWaterfallProps {
|
||||
traceId: string;
|
||||
@@ -100,7 +100,7 @@ function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element {
|
||||
switch (traceWaterfallState) {
|
||||
case TraceWaterfallStates.LOADING:
|
||||
return (
|
||||
<div className={styles.loadingSkeleton}>
|
||||
<div className="loading-skeleton">
|
||||
<Skeleton active paragraph={{ rows: 6 }} />
|
||||
</div>
|
||||
);
|
||||
@@ -158,7 +158,7 @@ function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element {
|
||||
uncollapsedNodes,
|
||||
]);
|
||||
|
||||
return <div className={styles.root}>{getContent}</div>;
|
||||
return <div className="trace-waterfall">{getContent}</div>;
|
||||
}
|
||||
|
||||
export default TraceWaterfall;
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
.root {
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
margin: 20px;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
border-radius: 4px;
|
||||
background: var(--destructive);
|
||||
}
|
||||
|
||||
.text,
|
||||
.value {
|
||||
color: var(--destructive-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.text {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
.error-waterfall {
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
margin: 20px;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
border-radius: 4px;
|
||||
background: var(--destructive);
|
||||
|
||||
.text {
|
||||
color: var(--destructive-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--destructive-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
import styles from './Error.module.scss';
|
||||
import './Error.styles.scss';
|
||||
|
||||
interface IErrorProps {
|
||||
error: AxiosError;
|
||||
@@ -12,16 +12,10 @@ function Error(props: IErrorProps): JSX.Element {
|
||||
const { error } = props;
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<Typography.Text className={styles.text}>
|
||||
Something went wrong!
|
||||
</Typography.Text>
|
||||
<div className="error-waterfall">
|
||||
<Typography.Text className="text">Something went wrong!</Typography.Text>
|
||||
<Tooltip title={error?.message}>
|
||||
<Typography.Text
|
||||
className={styles.value}
|
||||
title={error?.message}
|
||||
truncate={1}
|
||||
>
|
||||
<Typography.Text className="value" title={error?.message} truncate={1}>
|
||||
{error?.message}
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
// QuerySearch child sets `query-builder-search-v2` globally; size it to the
|
||||
// search container by reaching into the descendant.
|
||||
:global(.query-builder-search-v2) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// ToggleGroup children use generated class names; nest the global selectors
|
||||
// under the local row so they only apply inside this filter row.
|
||||
:global([class*='toggle-group']) {
|
||||
flex-shrink: 0;
|
||||
|
||||
:global([class*='toggle-group-item']) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.isExpanded {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.searchContainer {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
max-width: 220px;
|
||||
min-width: 120px;
|
||||
height: 32px;
|
||||
background: var(--l1-background);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border);
|
||||
}
|
||||
}
|
||||
|
||||
.pillText {
|
||||
font-size: 12px;
|
||||
color: var(--l2-foreground);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pillIndicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-background);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pillPopover {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.pillPopoverHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pillPopoverExpression {
|
||||
font-family: 'Geist Mono', 'Fira Code', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--l1-foreground);
|
||||
word-break: break-all;
|
||||
padding: 6px 8px;
|
||||
background: var(--l2-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.collapseBtn {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.highlightErrorsToggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.preNextToggle {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preNextCount {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: auto;
|
||||
color: var(--l2-foreground);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.filterStatus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
color: var(--l2-foreground);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.hasError {
|
||||
color: var(--destructive);
|
||||
cursor: help;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
.trace-v3-filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
&.expanded {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.filter-search-container {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.query-builder-search-v2 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// --- Collapsed pill ---
|
||||
.filter-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
max-width: 220px;
|
||||
min-width: 120px;
|
||||
height: 32px;
|
||||
background: var(--l1-background);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border);
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 12px;
|
||||
color: var(--l2-foreground);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-background);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Collapsed pill popover ---
|
||||
.filter-pill-popover {
|
||||
max-width: 400px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__expression {
|
||||
font-family: 'Geist Mono', 'Fira Code', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--l1-foreground);
|
||||
word-break: break-all;
|
||||
padding: 6px 8px;
|
||||
background: var(--l2-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// --- ToggleGroup override: size to content, don't stretch items ---
|
||||
[class*='toggle-group'] {
|
||||
flex-shrink: 0;
|
||||
|
||||
[class*='toggle-group-item'] {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Collapse button ---
|
||||
.filter-collapse-btn {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
// --- Highlight errors toggle ---
|
||||
.highlight-errors-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// --- Prev/next navigation ---
|
||||
.pre-next-toggle {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 12px;
|
||||
|
||||
&__count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: auto;
|
||||
color: var(--l2-foreground);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
color: var(--l2-foreground);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
|
||||
&--error {
|
||||
color: var(--destructive);
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,13 @@ import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import {
|
||||
TooltipRoot,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { AxiosError } from 'axios';
|
||||
import cx from 'classnames';
|
||||
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
|
||||
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
@@ -43,7 +42,7 @@ import {
|
||||
useSpanCategoryFilter,
|
||||
} from './hooks/useSpanCategoryFilter';
|
||||
|
||||
import styles from './Filters.module.scss';
|
||||
import './Filters.styles.scss';
|
||||
|
||||
function prepareQuery(filters: TagFilter, traceID: string): Query {
|
||||
return {
|
||||
@@ -256,7 +255,7 @@ function Filters({
|
||||
);
|
||||
|
||||
const highlightErrorsToggle = (
|
||||
<div className={styles.highlightErrorsToggle}>
|
||||
<div className="highlight-errors-toggle">
|
||||
<Typography.Text>Highlight errors</Typography.Text>
|
||||
<Switch
|
||||
color="cherry"
|
||||
@@ -270,9 +269,9 @@ function Filters({
|
||||
<>
|
||||
{isFetching && <Loader className="animate-spin" />}
|
||||
{error && (
|
||||
<TooltipRoot>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className={cx(styles.filterStatus, styles.hasError)}>
|
||||
<span className="filter-status filter-status--error">
|
||||
<Info />
|
||||
API error
|
||||
</span>
|
||||
@@ -280,10 +279,10 @@ function Filters({
|
||||
<TooltipContent>
|
||||
{(error as AxiosError)?.message || 'Something went wrong'}
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!error && noData && (
|
||||
<Typography.Text className={styles.filterStatus}>
|
||||
<Typography.Text className="filter-status">
|
||||
No results found
|
||||
</Typography.Text>
|
||||
)}
|
||||
@@ -294,22 +293,22 @@ function Filters({
|
||||
if (!isExpanded) {
|
||||
const pill = (
|
||||
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
|
||||
<div className={styles.pill} onClick={onExpand}>
|
||||
<div className="filter-pill" onClick={onExpand}>
|
||||
<Search size={12} />
|
||||
<span className={styles.pillText}>{expression || 'Search...'}</span>
|
||||
{expression && <span className={styles.pillIndicator} />}
|
||||
<span className="filter-pill__text">{expression || 'Search...'}</span>
|
||||
{expression && <span className="filter-pill__indicator" />}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className={styles.root}>
|
||||
<div className="trace-v3-filter-row collapsed">
|
||||
{expression ? (
|
||||
<TooltipRoot>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{pill}</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="start">
|
||||
<div className={styles.pillPopover}>
|
||||
<div className={styles.pillPopoverHeader}>
|
||||
<div className="filter-pill-popover">
|
||||
<div className="filter-pill-popover__header">
|
||||
<Typography.Text>Search query</Typography.Text>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -326,10 +325,10 @@ function Filters({
|
||||
<Copy size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.pillPopoverExpression}>{expression}</div>
|
||||
<div className="filter-pill-popover__expression">{expression}</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</Tooltip>
|
||||
) : (
|
||||
pill
|
||||
)}
|
||||
@@ -343,7 +342,7 @@ function Filters({
|
||||
// --- EXPANDED VIEW ---
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className={cx(styles.root, styles.isExpanded)}>
|
||||
<div className="trace-v3-filter-row expanded">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={selectedCategory}
|
||||
@@ -362,7 +361,7 @@ function Filters({
|
||||
</ToggleGroup>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className={styles.searchContainer}
|
||||
className="filter-search-container"
|
||||
ref={containerRef}
|
||||
onBlur={(e): void => {
|
||||
if (!containerRef.current?.contains(e.relatedTarget as Node)) {
|
||||
@@ -383,8 +382,8 @@ function Filters({
|
||||
/>
|
||||
</div>
|
||||
{filteredSpanIds.length > 0 && (
|
||||
<div className={styles.preNextToggle}>
|
||||
<Typography.Text className={styles.preNextCount}>
|
||||
<div className="pre-next-toggle">
|
||||
<Typography.Text className="pre-next-toggle__count">
|
||||
{currentSearchedIndex + 1} / {filteredSpanIds.length}
|
||||
</Typography.Text>
|
||||
<Button
|
||||
@@ -417,7 +416,7 @@ function Filters({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
className={styles.collapseBtn}
|
||||
className="filter-collapse-btn"
|
||||
onClick={onCollapse}
|
||||
>
|
||||
<X size={14} />
|
||||
|
||||
@@ -1,634 +0,0 @@
|
||||
// Inside a .module.scss, postcss-modules auto-scopes `@keyframes` identifiers
|
||||
// and rewrites `animation`/`animation-name` references to match.
|
||||
@keyframes waterfallLoading {
|
||||
0% {
|
||||
opacity: 0.3;
|
||||
transform: scaleX(0.3);
|
||||
transform-origin: left;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scaleX(1);
|
||||
transform-origin: left;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scaleX(0.3);
|
||||
transform-origin: right;
|
||||
}
|
||||
}
|
||||
|
||||
.loadingBar {
|
||||
height: 2px;
|
||||
background: var(--primary);
|
||||
animation: waterfallLoading 1.5s ease-in-out infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Event-dot tooltip. Visually matches SpanHoverCard's popover; styles are
|
||||
// inlined rather than `compose`d from another module because postcss-modules
|
||||
// loads cross-module composes through the plain CSS parser, which chokes on
|
||||
// SCSS `//` comments in the target file.
|
||||
.popover {
|
||||
--tooltip-z-index: 1000;
|
||||
background-color: var(--l1-background);
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--l2-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.root {
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.missingSpans {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 44px;
|
||||
margin: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
background: rgba(69, 104, 220, 0.1);
|
||||
}
|
||||
|
||||
.leftInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.rightInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
color: var(--bg-robin-200);
|
||||
}
|
||||
}
|
||||
|
||||
.splitPanel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 0px 20px 0px 20px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.splitHeader {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
height: 25px;
|
||||
background-color: var(--l1-background);
|
||||
}
|
||||
|
||||
.sidebarHeader {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.resizeHandleHeader {
|
||||
width: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.statusHeader {
|
||||
width: 50px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.timelineHeader {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.splitBody {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Invisible IntersectionObserver targets pinned at the top and bottom of the
|
||||
// virtualized content. See `useBoundaryPagination`.
|
||||
.loadMoreSentinel {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loadMoreSentinelTop {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.loadMoreSentinelBottom {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
|
||||
// ResizableBox child renders with a global `.resizable-box__content` class
|
||||
// — give it independent horizontal scrolling.
|
||||
:global(.resizable-box__content) {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 0.3rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.treeTable {
|
||||
position: relative;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.treeRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.treeCell {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.treeRow:hover,
|
||||
.treeRow.hoveredSpan {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 20%,
|
||||
transparent
|
||||
) !important;
|
||||
|
||||
.spanOverview {
|
||||
background: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarResizeHandle {
|
||||
width: 4px;
|
||||
flex-shrink: 0;
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
background: transparent;
|
||||
transition: background 0.15s ease;
|
||||
z-index: 1;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background: rgba(35, 196, 248, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.statusCol {
|
||||
width: 50px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.statusCell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
--badge-border-radius: 2px;
|
||||
--badge-padding: 3px 6px;
|
||||
--badge-line-height: 12px;
|
||||
--badge-border-width: 0px;
|
||||
|
||||
&.hoveredSpan {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 20%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
&.isInterested,
|
||||
&.isSelectedNonMatching {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 40%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
&.isDimmed {
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.crosshair {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: var(--l3-background);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.timelineRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// Match timelineHeader's 15px padding so bars align with ticks
|
||||
padding: 0 15px;
|
||||
|
||||
&:hover,
|
||||
&.hoveredSpan {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 20%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
&:has(.isInterested),
|
||||
&:has(.isSelectedNonMatching) {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 40%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.spanOverview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover .rowActions {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&.isInterested,
|
||||
&.isSelectedNonMatching {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 40%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.treeIndent {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.treeLine {
|
||||
position: absolute;
|
||||
background-color: var(--l2-border);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.treeConnector {
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 50%;
|
||||
border-left: 1px solid var(--l2-border);
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
border-bottom-left-radius: 6px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Reserved horizontal space for the chevron — present on every row, filled
|
||||
// only when the span has children. Keeps sibling icons aligned.
|
||||
.treeArrowSlot {
|
||||
width: 18px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.treeArrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
color: var(--l1-foreground);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
// Reserved horizontal space for the subtree-count badge — same reason.
|
||||
// Right-aligns the badge inside so single-digit counts don't push the icon
|
||||
// left of where multi-digit counts would put it.
|
||||
.subtreeCountSlot {
|
||||
min-width: 34px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.subtreeCount {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
:global(.badge) {
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
padding: 0 4px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.treeIcon {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
margin: 0 6px;
|
||||
|
||||
&.hasError {
|
||||
box-shadow: 0 0 0 2px rgba(255, 70, 70, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.treeLabel {
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Inter';
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 28px;
|
||||
letter-spacing: 0.01em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.treeServiceName {
|
||||
margin-left: 18px;
|
||||
color: var(--l3-foreground);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.rowActions {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-left: auto;
|
||||
padding-right: 4px;
|
||||
padding-left: 8px;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
background: linear-gradient(to left, var(--l1-background) 60%, transparent);
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.actionBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
color: var(--l1-foreground);
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
// Also reveal the actions when the parent tree row is hovered.
|
||||
.treeRow:hover .rowActions,
|
||||
.treeRow.hoveredSpan .rowActions {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.spanDuration {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 0 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.spanBar {
|
||||
position: absolute;
|
||||
height: 18px;
|
||||
top: 5px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
background-color: var(--span-color);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.spanInfo {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.spanName {
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.spanDurationText {
|
||||
color: inherit;
|
||||
opacity: 0.8;
|
||||
font-size: 10px;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.eventDot {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background-color: var(--event-dot-bg, var(--bg-robin-500));
|
||||
border: 1px solid var(--event-dot-border, var(--bg-robin-600));
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
|
||||
&.hasError {
|
||||
background-color: var(--destructive);
|
||||
border-color: var(--destructive);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translate(-50%, -50%) rotate(45deg) scale(1.5);
|
||||
}
|
||||
}
|
||||
|
||||
// Hover state: solid l2-background fill + border (matches flamegraph)
|
||||
.timelineRow:hover .spanBar,
|
||||
.timelineRow.hoveredSpan .spanBar {
|
||||
color: var(--span-color);
|
||||
background-color: var(--l2-background);
|
||||
background-image: none;
|
||||
border: 1px solid var(--span-color);
|
||||
}
|
||||
|
||||
// Selected state: solid l3-background fill + dashed border (matches flamegraph)
|
||||
.isInterested .spanBar,
|
||||
.isSelectedNonMatching .spanBar {
|
||||
color: var(--span-color);
|
||||
background-color: var(--l2-background);
|
||||
background-image: none;
|
||||
border: 1px dashed var(--span-color);
|
||||
}
|
||||
|
||||
.isDimmed {
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
.isHighlighted {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.isSelectedNonMatching {
|
||||
.treeLabel {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
// `.spanBar` text color is the one place where semantic tokens don't fit
|
||||
// cleanly: in dark mode the bar's bright `--span-color` background needs dark
|
||||
// text; in light mode `generateColor` produces darker bar fills, so the text
|
||||
// must flip to white.
|
||||
:global(.lightMode) {
|
||||
.root {
|
||||
.spanDuration .spanBar {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.timelineRow:hover .spanBar,
|
||||
.timelineRow.hoveredSpan .spanBar,
|
||||
.isInterested .spanBar,
|
||||
.isSelectedNonMatching .spanBar {
|
||||
color: var(--span-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltips for the row's hover-revealed action buttons (Copy / Add to Funnel).
|
||||
// Bumped above FloatingPanel (z-index 999) so they stay visible when the
|
||||
// SpanDetailsPanel is docked as a floating panel.
|
||||
.actionTooltip {
|
||||
--tooltip-z-index: 1000;
|
||||
}
|
||||
@@ -0,0 +1,636 @@
|
||||
.waterfall-loading-bar {
|
||||
height: 2px;
|
||||
background: var(--primary);
|
||||
animation: waterfall-loading 1.5s ease-in-out infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes waterfall-loading {
|
||||
0% {
|
||||
opacity: 0.3;
|
||||
transform: scaleX(0.3);
|
||||
transform-origin: left;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scaleX(1);
|
||||
transform-origin: left;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scaleX(0.3);
|
||||
transform-origin: right;
|
||||
}
|
||||
}
|
||||
|
||||
.success-content {
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.missing-spans {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 44px;
|
||||
margin: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
background: rgba(69, 104, 220, 0.1);
|
||||
|
||||
.left-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
.text {
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
|
||||
.right-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.right-info:hover {
|
||||
background-color: unset;
|
||||
color: var(--bg-robin-200);
|
||||
}
|
||||
}
|
||||
|
||||
.waterfall-split-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 0px 20px 0px 20px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.waterfall-split-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
height: 25px;
|
||||
background-color: var(--l1-background);
|
||||
|
||||
.sidebar-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.resize-handle-header {
|
||||
width: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-header {
|
||||
width: 50px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.waterfall-split-body {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Invisible IntersectionObserver targets pinned at the top and bottom of
|
||||
// the virtualized content. See `useBoundaryPagination`.
|
||||
.waterfall-load-more-sentinel {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
pointer-events: none;
|
||||
|
||||
&--top {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&--bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.waterfall-sidebar {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
|
||||
.resizable-box__content {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 0.3rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.span-tree-table {
|
||||
position: relative;
|
||||
border-collapse: collapse;
|
||||
|
||||
.span-tree-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.span-tree-cell {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.span-tree-row:hover,
|
||||
.span-tree-row.hovered-span {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 20%,
|
||||
transparent
|
||||
) !important;
|
||||
|
||||
.span-overview {
|
||||
background: unset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-resize-handle {
|
||||
width: 4px;
|
||||
flex-shrink: 0;
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
background: transparent;
|
||||
transition: background 0.15s ease;
|
||||
z-index: 1;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background: rgba(35, 196, 248, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.waterfall-status-col {
|
||||
width: 50px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
|
||||
.status-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
--badge-border-radius: 2px;
|
||||
--badge-padding: 3px 6px;
|
||||
--badge-line-height: 12px;
|
||||
--badge-border-width: 0px;
|
||||
|
||||
&.hovered-span {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 20%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
&.interested-span,
|
||||
&.selected-non-matching-span {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 40%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
&.dimmed-span {
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.waterfall-timeline {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.waterfall-crosshair {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: var(--l3-background);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.timeline-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// Match timeline-header's 15px padding so bars align with ticks
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.timeline-row:hover,
|
||||
.timeline-row.hovered-span {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 20%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
.timeline-row:has(.interested-span),
|
||||
.timeline-row:has(.selected-non-matching-span) {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 40%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Shared span component styles (used in both panels)
|
||||
.span-overview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
|
||||
.tree-indent {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tree-line {
|
||||
position: absolute;
|
||||
background-color: var(--l2-border);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tree-connector {
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 50%;
|
||||
border-left: 1px solid var(--l2-border);
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
border-bottom-left-radius: 6px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Reserved horizontal space for the chevron — present on every row,
|
||||
// filled only when the span has children. Keeps sibling icons aligned.
|
||||
.tree-arrow-slot {
|
||||
width: 18px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tree-arrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
color: var(--l1-foreground);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
// Reserved horizontal space for the subtree-count badge — same reason.
|
||||
// Right-aligns the badge inside so single-digit counts don't push the
|
||||
// icon left of where multi-digit counts would put it.
|
||||
.subtree-count-slot {
|
||||
min-width: 34px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.subtree-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.badge {
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
padding: 0 4px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-icon {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
margin: 0 6px;
|
||||
|
||||
&.is-error {
|
||||
box-shadow: 0 0 0 2px rgba(255, 70, 70, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.tree-label {
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Inter';
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 28px;
|
||||
letter-spacing: 0.01em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.tree-service-name {
|
||||
margin-left: 18px;
|
||||
color: var(--l3-foreground);
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.span-row-actions {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-left: auto;
|
||||
padding-right: 4px;
|
||||
padding-left: 8px;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
background: linear-gradient(to left, var(--l1-background) 60%, transparent);
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
.span-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
color: var(--l1-foreground);
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .span-row-actions {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Also show action buttons when hovering the tree row (parent of span-overview)
|
||||
.span-tree-row:hover .span-row-actions,
|
||||
.span-tree-row.hovered-span .span-row-actions {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.span-duration {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 0 15px;
|
||||
cursor: pointer;
|
||||
|
||||
.span-bar {
|
||||
position: absolute;
|
||||
height: 18px;
|
||||
top: 5px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
background-color: var(--span-color);
|
||||
border: 1px solid transparent;
|
||||
|
||||
.span-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
|
||||
.span-name {
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.span-duration-text {
|
||||
color: inherit;
|
||||
opacity: 0.8;
|
||||
font-size: 10px;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.event-dot {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background-color: var(--event-dot-bg, var(--bg-robin-500));
|
||||
border: 1px solid var(--event-dot-border, var(--bg-robin-600));
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
|
||||
&.error {
|
||||
background-color: var(--destructive);
|
||||
border-color: var(--destructive);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translate(-50%, -50%) rotate(45deg) scale(1.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hover state: solid l2-background fill + border (matches flamegraph)
|
||||
.timeline-row:hover .span-bar,
|
||||
.timeline-row.hovered-span .span-bar {
|
||||
color: var(--span-color);
|
||||
background-color: var(--l2-background);
|
||||
background-image: none;
|
||||
border: 1px solid var(--span-color);
|
||||
}
|
||||
|
||||
// Selected state: solid l3-background fill + dashed border (matches flamegraph)
|
||||
.interested-span .span-bar,
|
||||
.selected-non-matching-span .span-bar {
|
||||
color: var(--span-color);
|
||||
background-color: var(--l2-background);
|
||||
background-image: none;
|
||||
border: 1px dashed var(--span-color);
|
||||
}
|
||||
|
||||
// Shared state classes for both panels
|
||||
// Background highlight for selection is on .timeline-row via :has() — see .waterfall-timeline
|
||||
// Only apply on .span-overview (left panel) where there's no parent row with :has()
|
||||
.span-overview.interested-span,
|
||||
.span-overview.selected-non-matching-span {
|
||||
border-radius: 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--l3-background) 40%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
.dimmed-span {
|
||||
opacity: 0.15;
|
||||
}
|
||||
.highlighted-span {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.selected-non-matching-span {
|
||||
.tree-label {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.span-dets {
|
||||
.related-logs {
|
||||
display: flex;
|
||||
width: 160px;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// `.span-bar` text color is the one place where semantic tokens don't fit
|
||||
// cleanly: in dark mode the bar's bright `--span-color` background needs dark
|
||||
// text; in light mode `generateColor` produces darker bar fills, so the text
|
||||
// must flip to white. This is the only `.lightMode` carve-out left after the
|
||||
// migration to semantic tokens — the hover/selected rules are repeated here
|
||||
// to beat the default-state rule's specificity inside `.lightMode`.
|
||||
.lightMode {
|
||||
.success-content {
|
||||
.span-duration .span-bar {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.timeline-row:hover .span-bar,
|
||||
.timeline-row.hovered-span .span-bar,
|
||||
.interested-span .span-bar,
|
||||
.selected-non-matching-span .span-bar {
|
||||
color: var(--span-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltips for the row's hover-revealed action buttons (Copy / Add to Funnel).
|
||||
// Bumped above FloatingPanel (z-index 999) so they stay visible when the
|
||||
// SpanDetailsPanel is docked as a floating panel.
|
||||
.span-action-tooltip {
|
||||
--tooltip-z-index: 1000;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import {
|
||||
TooltipRoot,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
@@ -53,7 +53,7 @@ import { SpanHoverCard } from '../../../SpanHoverCard/SpanHoverCard';
|
||||
import AddSpanToFunnelModal from '../../AddSpanToFunnelModal/AddSpanToFunnelModal';
|
||||
import { IInterestedSpan } from '../../types';
|
||||
|
||||
import styles from './Success.module.scss';
|
||||
import './Success.styles.scss';
|
||||
|
||||
/**
|
||||
* Lazy event dot — only mounts the tooltip when the user hovers.
|
||||
@@ -91,7 +91,7 @@ const LazyEventDotPopover = memo(function LazyEventDotPopover({
|
||||
|
||||
const dot = (
|
||||
<div
|
||||
className={cx(styles.eventDot, isError && styles.hasError)}
|
||||
className={`event-dot ${isError ? 'error' : ''}`}
|
||||
style={
|
||||
{
|
||||
left: `${dotLeft}%`,
|
||||
@@ -112,16 +112,16 @@ const LazyEventDotPopover = memo(function LazyEventDotPopover({
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipRoot
|
||||
<Tooltip
|
||||
open
|
||||
onOpenChange={(open: boolean): void => {
|
||||
onOpenChange={(open): void => {
|
||||
if (!open) {
|
||||
setShowPopover(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TooltipTrigger asChild>{dot}</TooltipTrigger>
|
||||
<TooltipContent className={styles.popover}>
|
||||
<TooltipContent className="span-hover-card-popover">
|
||||
<EventTooltipContent
|
||||
eventName={event.name}
|
||||
timeOffsetMs={eventTimeMs - spanTimestamp}
|
||||
@@ -129,7 +129,7 @@ const LazyEventDotPopover = memo(function LazyEventDotPopover({
|
||||
attributeMap={event.attributeMap || {}}
|
||||
/>
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
});
|
||||
@@ -243,11 +243,11 @@ const SpanOverview = memo(function SpanOverview({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.spanOverview, {
|
||||
[styles.isInterested]: isSelected && (!isFilterActive || isMatching),
|
||||
[styles.isHighlighted]: isHighlighted,
|
||||
[styles.isSelectedNonMatching]: isSelectedNonMatching,
|
||||
[styles.isDimmed]: isDimmed,
|
||||
className={cx('span-overview', {
|
||||
'interested-span': isSelected && (!isFilterActive || isMatching),
|
||||
'highlighted-span': isHighlighted,
|
||||
'selected-non-matching-span': isSelectedNonMatching,
|
||||
'dimmed-span': isDimmed,
|
||||
})}
|
||||
onClick={(): void => handleSpanClick(span)}
|
||||
onMouseEnter={(): void => onHoverEnter(span.span_id)}
|
||||
@@ -264,7 +264,7 @@ const SpanOverview = memo(function SpanOverview({
|
||||
return (
|
||||
<div
|
||||
key={lvl}
|
||||
className={styles.treeLine}
|
||||
className="tree-line"
|
||||
style={{
|
||||
left: xPos,
|
||||
top: 0,
|
||||
@@ -277,25 +277,25 @@ const SpanOverview = memo(function SpanOverview({
|
||||
return (
|
||||
<div key={lvl}>
|
||||
<div
|
||||
className={styles.treeLine}
|
||||
className="tree-line"
|
||||
style={{ left: xPos, top: 0, width: 1, height: '50%' }}
|
||||
/>
|
||||
<div className={styles.treeConnector} style={{ left: xPos, top: 0 }} />
|
||||
<div className="tree-connector" style={{ left: xPos, top: 0 }} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Indent spacer */}
|
||||
<span className={styles.treeIndent} style={{ width: `${indentWidth}px` }} />
|
||||
<span className="tree-indent" style={{ width: `${indentWidth}px` }} />
|
||||
|
||||
{/* Expand/collapse arrow + child count slots — always render the
|
||||
slots, fill them only when the span has children. Reserving the
|
||||
horizontal space on leaf rows aligns sibling icons regardless
|
||||
of whether each sibling is a parent or a leaf. */}
|
||||
<span className={styles.treeArrowSlot}>
|
||||
<span className="tree-arrow-slot">
|
||||
{span.has_children && (
|
||||
<span
|
||||
className={styles.treeArrow}
|
||||
className={cx('tree-arrow', { expanded: !isSpanCollapsed })}
|
||||
onClick={(event): void => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
@@ -306,9 +306,9 @@ const SpanOverview = memo(function SpanOverview({
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className={styles.subtreeCountSlot}>
|
||||
<span className="subtree-count-slot">
|
||||
{span.has_children && (
|
||||
<span className={styles.subtreeCount}>
|
||||
<span className="subtree-count">
|
||||
<Badge color="vanilla">{span.sub_tree_node_count}</Badge>
|
||||
</span>
|
||||
)}
|
||||
@@ -316,51 +316,51 @@ const SpanOverview = memo(function SpanOverview({
|
||||
|
||||
{/* Colored service dot */}
|
||||
<span
|
||||
className={cx(styles.treeIcon, { [styles.hasError]: span.has_error })}
|
||||
className={cx('tree-icon', { 'is-error': span.has_error })}
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
|
||||
{/* Span name + service name */}
|
||||
<span className={styles.treeLabel}>
|
||||
<span className="tree-label">
|
||||
{span.name}
|
||||
<span className={styles.treeServiceName}>{span['service.name']}</span>
|
||||
<span className="tree-service-name">{span['service.name']}</span>
|
||||
</span>
|
||||
|
||||
{/* Action buttons — shown on hover via CSS, right-aligned */}
|
||||
<span className={styles.rowActions}>
|
||||
<span className="span-row-actions">
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<TooltipRoot>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
className={styles.actionBtn}
|
||||
className="span-action-btn"
|
||||
onClick={onSpanCopy}
|
||||
>
|
||||
<Link size={12} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={styles.actionTooltip}>
|
||||
<TooltipContent className="span-action-tooltip">
|
||||
Copy Span Link
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
<TooltipRoot>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
className={styles.actionBtn}
|
||||
className="span-action-btn"
|
||||
onClick={handleFunnelClick}
|
||||
>
|
||||
<ListPlus size={12} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={styles.actionTooltip}>
|
||||
<TooltipContent className="span-action-tooltip">
|
||||
Add to Trace Funnel
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</span>
|
||||
</div>
|
||||
@@ -410,16 +410,16 @@ export const SpanDuration = memo(function SpanDuration({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.spanDuration, {
|
||||
[styles.isInterested]: isSelected && (!isFilterActive || isMatching),
|
||||
[styles.isHighlighted]: isHighlighted,
|
||||
[styles.isSelectedNonMatching]: isSelectedNonMatching,
|
||||
[styles.isDimmed]: isDimmed,
|
||||
className={cx('span-duration', {
|
||||
'interested-span': isSelected && (!isFilterActive || isMatching),
|
||||
'highlighted-span': isHighlighted,
|
||||
'selected-non-matching-span': isSelectedNonMatching,
|
||||
'dimmed-span': isDimmed,
|
||||
})}
|
||||
onClick={(): void => handleSpanClick(span)}
|
||||
>
|
||||
<div
|
||||
className={styles.spanBar}
|
||||
className="span-bar"
|
||||
style={
|
||||
{
|
||||
left: `${leftOffset}%`,
|
||||
@@ -429,9 +429,9 @@ export const SpanDuration = memo(function SpanDuration({
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<span className={styles.spanInfo}>
|
||||
<span className={styles.spanName}>{span.name}</span>
|
||||
<span className={styles.spanDurationText}>{`${toFixed(
|
||||
<span className="span-info">
|
||||
<span className="span-name">{span.name}</span>
|
||||
<span className="span-duration-text">{`${toFixed(
|
||||
time,
|
||||
2,
|
||||
)} ${timeUnitName}`}</span>
|
||||
@@ -529,11 +529,11 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
|
||||
if (prev) {
|
||||
const prevElements = document.querySelectorAll(`[data-span-id="${prev}"]`);
|
||||
prevElements.forEach((el) => el.classList.remove(styles.hoveredSpan));
|
||||
prevElements.forEach((el) => el.classList.remove('hovered-span'));
|
||||
}
|
||||
if (spanId) {
|
||||
const nextElements = document.querySelectorAll(`[data-span-id="${spanId}"]`);
|
||||
nextElements.forEach((el) => el.classList.add(styles.hoveredSpan));
|
||||
nextElements.forEach((el) => el.classList.add('hovered-span'));
|
||||
}
|
||||
prevHoveredSpanIdRef.current = spanId;
|
||||
}, []);
|
||||
@@ -798,17 +798,17 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className="success-content">
|
||||
{traceMetadata.hasMissingSpans && (
|
||||
<div className={styles.missingSpans}>
|
||||
<section className={styles.leftInfo}>
|
||||
<div className="missing-spans">
|
||||
<section className="left-info">
|
||||
<CircleAlert size={14} />
|
||||
<span className={styles.text}>This trace has missing spans</span>
|
||||
<span className="text">This trace has missing spans</span>
|
||||
</section>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className={styles.rightInfo}
|
||||
className="right-info"
|
||||
suffix={<ArrowUpRight size={14} />}
|
||||
onClick={(): WindowProxy | null =>
|
||||
window.open(
|
||||
@@ -821,17 +821,17 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isFetching && <div className={styles.loadingBar} />}
|
||||
<div className={styles.splitPanel} ref={scrollContainerRef}>
|
||||
{isFetching && <div className="waterfall-loading-bar" />}
|
||||
<div className="waterfall-split-panel" ref={scrollContainerRef}>
|
||||
{/* Sticky header row */}
|
||||
<div className={styles.splitHeader}>
|
||||
<div className="waterfall-split-header">
|
||||
<div
|
||||
className={styles.sidebarHeader}
|
||||
className="sidebar-header"
|
||||
style={{ width: sidebarWidth, flexShrink: 0 }}
|
||||
/>
|
||||
<div className={styles.resizeHandleHeader} />
|
||||
<div className={styles.statusHeader} />
|
||||
<div className={styles.timelineHeader}>
|
||||
<div className="resize-handle-header" />
|
||||
<div className="status-header" />
|
||||
<div className="timeline-header">
|
||||
<TimelineV3
|
||||
startTimestamp={traceMetadata.startTime}
|
||||
endTimestamp={traceMetadata.endTime}
|
||||
@@ -844,7 +844,7 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
|
||||
{/* Split body */}
|
||||
<div
|
||||
className={styles.splitBody}
|
||||
className="waterfall-split-body"
|
||||
style={{
|
||||
minHeight: virtualizer.getTotalSize(),
|
||||
height: '100%',
|
||||
@@ -854,11 +854,11 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
fires a load-more via useBoundaryPagination. */}
|
||||
<div
|
||||
ref={loadMoreTopSentinelRef}
|
||||
className={cx(styles.loadMoreSentinel, styles.loadMoreSentinelTop)}
|
||||
className="waterfall-load-more-sentinel waterfall-load-more-sentinel--top"
|
||||
/>
|
||||
<div
|
||||
ref={loadMoreBottomSentinelRef}
|
||||
className={cx(styles.loadMoreSentinel, styles.loadMoreSentinelBottom)}
|
||||
className="waterfall-load-more-sentinel waterfall-load-more-sentinel--bottom"
|
||||
/>
|
||||
<SpanHoverCard
|
||||
hoveredSpanId={hoveredSpanId}
|
||||
@@ -875,9 +875,9 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
minWidth={MIN_SIDEBAR_WIDTH}
|
||||
maxWidth={MAX_SIDEBAR_WIDTH}
|
||||
onResize={setSidebarWidth}
|
||||
className={styles.sidebar}
|
||||
className="waterfall-sidebar"
|
||||
>
|
||||
<table className={styles.treeTable} style={{ width: maxContentWidth }}>
|
||||
<table className="span-tree-table" style={{ width: maxContentWidth }}>
|
||||
<tbody>
|
||||
{virtualItems.map((virtualRow) => {
|
||||
const row = leftRows[virtualRow.index];
|
||||
@@ -887,7 +887,7 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
key={String(virtualRow.key)}
|
||||
data-testid={`cell-0-${span.span_id}`}
|
||||
data-span-id={span.span_id}
|
||||
className={styles.treeRow}
|
||||
className="span-tree-row"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
@@ -900,7 +900,7 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
onMouseLeave={handleRowMouseLeave}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td key={cell.id} className={styles.treeCell}>
|
||||
<td key={cell.id} className="span-tree-cell">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
@@ -912,7 +912,7 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
</ResizableBox>
|
||||
|
||||
{/* Status code column */}
|
||||
<div className={styles.statusCol}>
|
||||
<div className="waterfall-status-col">
|
||||
{virtualItems.map((virtualRow) => {
|
||||
const span = spans[virtualRow.index];
|
||||
const { isSelected, isDimmed, isSelectedNonMatching, isMatching } =
|
||||
@@ -925,10 +925,10 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
key={`status-${String(virtualRow.key)}`}
|
||||
className={cx(styles.statusCell, {
|
||||
[styles.isInterested]: isSelected && (!isFilterActive || isMatching),
|
||||
[styles.isDimmed]: isDimmed,
|
||||
[styles.isSelectedNonMatching]: isSelectedNonMatching,
|
||||
className={cx('status-cell', {
|
||||
'interested-span': isSelected && (!isFilterActive || isMatching),
|
||||
'dimmed-span': isDimmed,
|
||||
'selected-non-matching-span': isSelectedNonMatching,
|
||||
})}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@@ -953,13 +953,13 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
|
||||
{/* Right panel - timeline bars */}
|
||||
<div
|
||||
className={styles.timeline}
|
||||
className="waterfall-timeline"
|
||||
ref={timelineAreaRef}
|
||||
onMouseMove={onCrosshairMove}
|
||||
onMouseLeave={onCrosshairLeave}
|
||||
>
|
||||
{cursorX !== null && (
|
||||
<div className={styles.crosshair} style={{ left: cursorX }} />
|
||||
<div className="waterfall-crosshair" style={{ left: cursorX }} />
|
||||
)}
|
||||
{virtualItems.map((virtualRow) => {
|
||||
const span = spans[virtualRow.index];
|
||||
@@ -968,7 +968,7 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
key={String(virtualRow.key)}
|
||||
data-testid={`cell-1-${span.span_id}`}
|
||||
data-span-id={span.span_id}
|
||||
className={styles.timelineRow}
|
||||
className="timeline-row"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
|
||||
@@ -2,33 +2,18 @@ import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { fireEvent, render, screen } from 'tests/test-utils';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
|
||||
// Local identity-proxy mock for this module so `styles.foo` resolves to
|
||||
// `'foo'` in test assertions. The global `__mocks__/cssMock.ts` stays as
|
||||
// `export default {}`; we override resolution for this specific file only.
|
||||
jest.mock(
|
||||
'../Success.module.scss',
|
||||
() =>
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, prop): string | undefined =>
|
||||
typeof prop === 'string' && prop !== '__esModule' ? prop : undefined,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
import { SpanDuration } from '../Success';
|
||||
import successStyles from '../Success.module.scss';
|
||||
|
||||
const renderWithTraceProvider: typeof render = (ui, options) =>
|
||||
render(ui, options);
|
||||
|
||||
// Constants to avoid string duplication
|
||||
const SPAN_DURATION_TEXT = '1.16 ms';
|
||||
const SPAN_DURATION_CLASS = `.${successStyles.spanDuration}`;
|
||||
const INTERESTED_SPAN_CLASS = successStyles.isInterested;
|
||||
const HIGHLIGHTED_SPAN_CLASS = successStyles.isHighlighted;
|
||||
const DIMMED_SPAN_CLASS = successStyles.isDimmed;
|
||||
const SELECTED_NON_MATCHING_SPAN_CLASS = successStyles.isSelectedNonMatching;
|
||||
const SPAN_DURATION_CLASS = '.span-duration';
|
||||
const INTERESTED_SPAN_CLASS = 'interested-span';
|
||||
const HIGHLIGHTED_SPAN_CLASS = 'highlighted-span';
|
||||
const DIMMED_SPAN_CLASS = 'dimmed-span';
|
||||
const SELECTED_NON_MATCHING_SPAN_CLASS = 'selected-non-matching-span';
|
||||
|
||||
jest.mock('components/TimelineV3/TimelineV3', () => ({
|
||||
__esModule: true,
|
||||
@@ -142,7 +127,10 @@ describe('SpanDuration', () => {
|
||||
|
||||
const spanElement = screen.getByText(SPAN_DURATION_TEXT);
|
||||
|
||||
// Hover over the span should add hovered-span class
|
||||
fireEvent.mouseEnter(spanElement);
|
||||
|
||||
// Mouse leave should remove hovered-span class
|
||||
fireEvent.mouseLeave(spanElement);
|
||||
});
|
||||
|
||||
@@ -271,8 +259,8 @@ describe('SpanDuration', () => {
|
||||
traceMetadata={mockTraceMetadata}
|
||||
selectedSpan={undefined}
|
||||
handleSpanClick={mockSetSelectedSpan}
|
||||
filteredSpanIds={[]}
|
||||
isFilterActive
|
||||
filteredSpanIds={[]} // Empty array but filter is active
|
||||
isFilterActive // This is the key difference
|
||||
/>,
|
||||
);
|
||||
|
||||
|
||||
@@ -2,23 +2,7 @@ import React from 'react';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
|
||||
// Local identity-proxy mock for this module so `styles.foo` resolves to
|
||||
// `'foo'` in test assertions. The global `__mocks__/cssMock.ts` stays as
|
||||
// `export default {}`; we override resolution for this specific file only.
|
||||
jest.mock(
|
||||
'../Success.module.scss',
|
||||
() =>
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, prop): string | undefined =>
|
||||
typeof prop === 'string' && prop !== '__esModule' ? prop : undefined,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
import Success from '../Success';
|
||||
import successStyles from '../Success.module.scss';
|
||||
|
||||
const renderWithTraceProvider: typeof render = (ui, options, customOptions) =>
|
||||
render(ui, options, customOptions);
|
||||
@@ -227,10 +211,10 @@ describe('Span Click User Flows', () => {
|
||||
const FIRST_SPAN_TEST_ID = 'cell-0-span-1';
|
||||
const FIRST_SPAN_DURATION_TEST_ID = 'cell-1-span-1';
|
||||
const SECOND_SPAN_TEST_ID = 'cell-0-span-2';
|
||||
const SPAN_OVERVIEW_CLASS = '.span-overview';
|
||||
const SPAN_DURATION_CLASS = '.span-duration';
|
||||
const INTERESTED_SPAN_CLASS = 'interested-span';
|
||||
const SECOND_SPAN_DURATION_TEST_ID = 'cell-1-span-2';
|
||||
const SPAN_OVERVIEW_CLASS = `.${successStyles.spanOverview}`;
|
||||
const SPAN_DURATION_CLASS = `.${successStyles.spanDuration}`;
|
||||
const INTERESTED_SPAN_CLASS = successStyles.isInterested;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -284,6 +268,7 @@ describe('Span Click User Flows', () => {
|
||||
initialRoute: '/trace',
|
||||
});
|
||||
|
||||
// Wait for initial render and selection
|
||||
await waitFor(() => {
|
||||
const spanDuration = screen.getByTestId(FIRST_SPAN_DURATION_TEST_ID);
|
||||
const spanDurationElement = spanDuration.querySelector(
|
||||
@@ -292,12 +277,14 @@ describe('Span Click User Flows', () => {
|
||||
expect(spanDurationElement).toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
});
|
||||
|
||||
// Click on span-2 to test selection change
|
||||
const span2Duration = screen.getByTestId(SECOND_SPAN_DURATION_TEST_ID);
|
||||
const span2DurationElement = span2Duration.querySelector(
|
||||
SPAN_DURATION_CLASS,
|
||||
) as HTMLElement;
|
||||
await user.click(span2DurationElement);
|
||||
|
||||
// Wait for the state update and re-render
|
||||
await waitFor(() => {
|
||||
const spanDuration = screen.getByTestId(FIRST_SPAN_DURATION_TEST_ID);
|
||||
const spanDurationElement = spanDuration.querySelector(
|
||||
@@ -320,6 +307,7 @@ describe('Span Click User Flows', () => {
|
||||
initialRoute: '/trace',
|
||||
});
|
||||
|
||||
// Wait for initial render and selection
|
||||
await waitFor(() => {
|
||||
const spanOverview = screen.getByTestId(FIRST_SPAN_TEST_ID);
|
||||
const spanDuration = screen.getByTestId(FIRST_SPAN_DURATION_TEST_ID);
|
||||
@@ -330,16 +318,19 @@ describe('Span Click User Flows', () => {
|
||||
SPAN_DURATION_CLASS,
|
||||
) as HTMLElement;
|
||||
|
||||
// Initially both areas should show the same visual selection (first span is auto-selected)
|
||||
expect(spanOverviewElement).toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
expect(spanDurationElement).toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
});
|
||||
|
||||
// Click span-2 to test selection change
|
||||
const span2Overview = screen.getByTestId(SECOND_SPAN_TEST_ID);
|
||||
const span2Element = span2Overview.querySelector(
|
||||
SPAN_OVERVIEW_CLASS,
|
||||
) as HTMLElement;
|
||||
await user.click(span2Element);
|
||||
|
||||
// Wait for the state update and re-render
|
||||
await waitFor(() => {
|
||||
const spanOverview = screen.getByTestId(FIRST_SPAN_TEST_ID);
|
||||
const spanDuration = screen.getByTestId(FIRST_SPAN_DURATION_TEST_ID);
|
||||
@@ -350,15 +341,17 @@ describe('Span Click User Flows', () => {
|
||||
SPAN_DURATION_CLASS,
|
||||
) as HTMLElement;
|
||||
|
||||
// Now span-2 should be selected, span-1 should not
|
||||
expect(spanOverviewElement).not.toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
expect(spanDurationElement).not.toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
|
||||
const span2OverviewSub = screen.getByTestId(SECOND_SPAN_TEST_ID);
|
||||
const span2DurationSub = screen.getByTestId(SECOND_SPAN_DURATION_TEST_ID);
|
||||
const span2OverviewElement = span2OverviewSub.querySelector(
|
||||
// Check that span-2 is selected
|
||||
const span2Overview = screen.getByTestId(SECOND_SPAN_TEST_ID);
|
||||
const span2Duration = screen.getByTestId(SECOND_SPAN_DURATION_TEST_ID);
|
||||
const span2OverviewElement = span2Overview.querySelector(
|
||||
SPAN_OVERVIEW_CLASS,
|
||||
) as HTMLElement;
|
||||
const span2DurationElement = span2DurationSub.querySelector(
|
||||
const span2DurationElement = span2Duration.querySelector(
|
||||
SPAN_DURATION_CLASS,
|
||||
) as HTMLElement;
|
||||
|
||||
@@ -374,6 +367,7 @@ describe('Span Click User Flows', () => {
|
||||
initialRoute: '/trace',
|
||||
});
|
||||
|
||||
// Wait for initial render and selection
|
||||
await waitFor(() => {
|
||||
const span1Overview = screen.getByTestId(FIRST_SPAN_TEST_ID);
|
||||
const span1Element = span1Overview.querySelector(
|
||||
@@ -382,24 +376,27 @@ describe('Span Click User Flows', () => {
|
||||
expect(span1Element).toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
});
|
||||
|
||||
// Click second span
|
||||
const span2Overview = screen.getByTestId(SECOND_SPAN_TEST_ID);
|
||||
const span2Element = span2Overview.querySelector(
|
||||
SPAN_OVERVIEW_CLASS,
|
||||
) as HTMLElement;
|
||||
await user.click(span2Element);
|
||||
|
||||
// Wait for the state update and re-render
|
||||
await waitFor(() => {
|
||||
const span1Overview = screen.getByTestId(FIRST_SPAN_TEST_ID);
|
||||
const span1Element = span1Overview.querySelector(
|
||||
SPAN_OVERVIEW_CLASS,
|
||||
) as HTMLElement;
|
||||
const span2OverviewSub = screen.getByTestId(SECOND_SPAN_TEST_ID);
|
||||
const span2ElementSub = span2OverviewSub.querySelector(
|
||||
const span2Overview = screen.getByTestId(SECOND_SPAN_TEST_ID);
|
||||
const span2Element = span2Overview.querySelector(
|
||||
SPAN_OVERVIEW_CLASS,
|
||||
) as HTMLElement;
|
||||
|
||||
// Second span should be selected, first should not
|
||||
expect(span1Element).not.toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
expect(span2ElementSub).toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
expect(span2Element).toHaveClass(INTERESTED_SPAN_CLASS);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -429,6 +426,7 @@ describe('Span Click User Flows', () => {
|
||||
{ initialRoute: '/trace' },
|
||||
);
|
||||
|
||||
// Click on the actual span element (not the wrapper)
|
||||
const spanOverview = screen.getByTestId(FIRST_SPAN_TEST_ID);
|
||||
const spanElement = spanOverview.querySelector(
|
||||
SPAN_OVERVIEW_CLASS,
|
||||
@@ -440,6 +438,7 @@ describe('Span Click User Flows', () => {
|
||||
expect(mockUrlQuery.get('anotherParam')).toBe('anotherValue');
|
||||
expect(mockUrlQuery.get('spanId')).toBe('span-1');
|
||||
|
||||
// Verify navigation was called with all parameters
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith({
|
||||
search: expect.stringMatching(
|
||||
/existingParam=existingValue.*anotherParam=anotherValue.*spanId=span-1/,
|
||||
|
||||
@@ -15,13 +15,10 @@ import {
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import cx from 'classnames';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { GripVertical } from '@signozhq/icons';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import styles from './FieldsSettings.module.scss';
|
||||
|
||||
function SortableField({
|
||||
field,
|
||||
onRemove,
|
||||
@@ -43,17 +40,14 @@ function SortableField({
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cx(
|
||||
styles.fieldItem,
|
||||
allowDrag ? styles.isDragEnabled : styles.isDragDisabled,
|
||||
)}
|
||||
className={`fs-field-item ${allowDrag ? 'drag-enabled' : 'drag-disabled'}`}
|
||||
>
|
||||
<div {...attributes} {...listeners} className={styles.dragHandle}>
|
||||
<div {...attributes} {...listeners} className="drag-handle">
|
||||
{allowDrag && <GripVertical size={14} />}
|
||||
<span className={styles.fieldKey}>{field.key}</span>
|
||||
<span className="fs-field-key">{field.key}</span>
|
||||
</div>
|
||||
<Button
|
||||
className={cx(styles.removeBtn, 'periscope-btn')}
|
||||
className="remove-field-btn periscope-btn"
|
||||
variant="outlined"
|
||||
color="destructive"
|
||||
size="sm"
|
||||
@@ -100,9 +94,9 @@ function AddedFields({
|
||||
const allowDrag = inputValue.length === 0;
|
||||
|
||||
return (
|
||||
<div className={cx(styles.section, styles.sectionAdded)}>
|
||||
<div className={styles.sectionHeader}>ADDED FIELDS</div>
|
||||
<div className={styles.addedList}>
|
||||
<div className="fs-section fs-added">
|
||||
<div className="fs-section-header">ADDED FIELDS</div>
|
||||
<div className="fs-added-list">
|
||||
<OverlayScrollbar>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
@@ -110,7 +104,7 @@ function AddedFields({
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{filteredFields.length === 0 ? (
|
||||
<div className={styles.noValues}>No values found</div>
|
||||
<div className="fs-no-values">No values found</div>
|
||||
) : (
|
||||
<SortableContext
|
||||
items={fields.map((f) => f.key)}
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
cursor: pointer;
|
||||
color: var(--l2-foreground);
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
background-color: var(--l1-background);
|
||||
height: 40px;
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sectionAdded {
|
||||
max-height: 40%;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.sectionOther {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.addedList {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.otherList {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
// Ant Skeleton.Input rendered inside the loading state — override its
|
||||
// hard-coded width.
|
||||
:global(.ant-skeleton-input) {
|
||||
width: 300px;
|
||||
margin: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.noValues {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.limitHint {
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.fieldItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
font-size: 13px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--l2-background);
|
||||
|
||||
.removeBtn,
|
||||
.addBtn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dragHandle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.fieldKey {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.isDragEnabled {
|
||||
cursor: grab;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.isDragDisabled {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.otherFieldItem {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.removeBtn,
|
||||
.addBtn {
|
||||
padding: 4px 10px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
justify-content: space-between;
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
.fields-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
overflow: hidden;
|
||||
|
||||
.fs-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.fs-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fs-close-icon {
|
||||
cursor: pointer;
|
||||
color: var(--l2-foreground);
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fs-search {
|
||||
.fs-search-input {
|
||||
background-color: var(--l1-background);
|
||||
height: 40px;
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fs-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.fs-added {
|
||||
max-height: 40%;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
&.fs-other {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fs-section-header {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.fs-added-list {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fs-other-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-skeleton-input {
|
||||
width: 300px;
|
||||
margin: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.fs-no-values {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.fs-limit-hint {
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.fs-field-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
font-size: 13px;
|
||||
|
||||
.drag-handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.fs-field-key {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.drag-enabled {
|
||||
cursor: grab;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
&.drag-disabled {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
&.other-field-item {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.remove-field-btn,
|
||||
.add-field-btn {
|
||||
padding: 4px 10px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--l2-background);
|
||||
|
||||
.remove-field-btn,
|
||||
.add-field-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fs-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
justify-content: space-between;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import AddedFields from './AddedFields';
|
||||
import OtherFields from './OtherFields';
|
||||
|
||||
import styles from './FieldsSettings.module.scss';
|
||||
import './FieldsSettings.styles.scss';
|
||||
|
||||
const MAX_FIELDS_DEFAULT = 10;
|
||||
|
||||
@@ -89,18 +89,18 @@ function FieldsSettings({
|
||||
const isAtLimit = draftFields.length >= maxFields;
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>
|
||||
<div className="fields-settings">
|
||||
<div className="fs-header">
|
||||
<div className="fs-title">
|
||||
<TableColumnsSplit size={16} />
|
||||
{title}
|
||||
</div>
|
||||
<X className={styles.closeIcon} size={16} onClick={onClose} />
|
||||
<X className="fs-close-icon" size={16} onClick={onClose} />
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<section className="fs-search">
|
||||
<Input
|
||||
className={styles.searchInput}
|
||||
className="fs-search-input"
|
||||
type="text"
|
||||
value={inputValue}
|
||||
placeholder="Search for a field..."
|
||||
@@ -123,7 +123,7 @@ function FieldsSettings({
|
||||
/>
|
||||
|
||||
{hasUnsavedChanges && (
|
||||
<div className={styles.footer}>
|
||||
<div className="fs-footer">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Skeleton } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import styles from './FieldsSettings.module.scss';
|
||||
|
||||
interface OtherFieldsProps {
|
||||
dataSource: DataSource;
|
||||
debouncedInputValue: string;
|
||||
@@ -53,9 +50,9 @@ function OtherFields({
|
||||
|
||||
if (isFetching) {
|
||||
return (
|
||||
<div className={cx(styles.section, styles.sectionOther)}>
|
||||
<div className={styles.sectionHeader}>OTHER FIELDS</div>
|
||||
<div className={styles.otherList}>
|
||||
<div className="fs-section fs-other">
|
||||
<div className="fs-section-header">OTHER FIELDS</div>
|
||||
<div className="fs-other-list">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Skeleton.Input active size="small" key={i} />
|
||||
@@ -66,23 +63,20 @@ function OtherFields({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(styles.section, styles.sectionOther)}>
|
||||
<div className={styles.sectionHeader}>OTHER FIELDS</div>
|
||||
<div className={styles.otherList}>
|
||||
<div className="fs-section fs-other">
|
||||
<div className="fs-section-header">OTHER FIELDS</div>
|
||||
<div className="fs-other-list">
|
||||
<OverlayScrollbar>
|
||||
<>
|
||||
{otherFields.length === 0 ? (
|
||||
<div className={styles.noValues}>No values found</div>
|
||||
<div className="fs-no-values">No values found</div>
|
||||
) : (
|
||||
otherFields.map((attr) => (
|
||||
<div
|
||||
key={attr.key}
|
||||
className={cx(styles.fieldItem, styles.otherFieldItem)}
|
||||
>
|
||||
<span className={styles.fieldKey}>{attr.key}</span>
|
||||
<div key={attr.key} className="fs-field-item other-field-item">
|
||||
<span className="fs-field-key">{attr.key}</span>
|
||||
{!isAtLimit && (
|
||||
<Button
|
||||
className={cx(styles.addBtn, 'periscope-btn')}
|
||||
className="add-field-btn periscope-btn"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
@@ -94,7 +88,7 @@ function OtherFields({
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{isAtLimit && <div className={styles.limitHint}>Maximum 10 fields</div>}
|
||||
{isAtLimit && <div className="fs-limit-hint">Maximum 10 fields</div>}
|
||||
</>
|
||||
</OverlayScrollbar>
|
||||
</div>
|
||||
|
||||
@@ -32,9 +32,7 @@ import TraceWaterfall from './TraceWaterfall/TraceWaterfall';
|
||||
import { IInterestedSpan } from './TraceWaterfall/types';
|
||||
import { getAncestorSpanIds } from './TraceWaterfall/utils';
|
||||
|
||||
import cx from 'classnames';
|
||||
|
||||
import styles from './TraceDetailsV3.module.scss';
|
||||
import './TraceDetailsV3.styles.scss';
|
||||
|
||||
function TraceDetailsV3(): JSX.Element {
|
||||
const { id: traceId } = useParams<TraceDetailV3URLProps>();
|
||||
@@ -287,7 +285,7 @@ function TraceDetailsV3(): JSX.Element {
|
||||
|
||||
return (
|
||||
<TraceStoreSync aggregations={traceData?.payload?.aggregations}>
|
||||
<div className={styles.root}>
|
||||
<div className="trace-details-v3">
|
||||
<TraceDetailsHeader
|
||||
filterMetadata={filterMetadata}
|
||||
onFilteredSpansChange={handleFilteredSpansChange}
|
||||
@@ -299,20 +297,20 @@ function TraceDetailsV3(): JSX.Element {
|
||||
<NoData />
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.content}>
|
||||
<div className="trace-details-v3__content">
|
||||
<Collapse
|
||||
// @ts-expect-error motion is passed through to rc-collapse to disable animation
|
||||
motion={false}
|
||||
activeKey={activeKeys.filter((k) => k === 'flame')}
|
||||
onChange={(): void => handleCollapseChange('flame')}
|
||||
size="small"
|
||||
className={styles.flameCollapse}
|
||||
className="trace-details-v3__flame-collapse"
|
||||
items={[
|
||||
{
|
||||
key: 'flame',
|
||||
label: (
|
||||
<div className={styles.collapseLabel}>
|
||||
<span className={styles.collapseTitle}>
|
||||
<div className="trace-details-v3__collapse-label">
|
||||
<span className="trace-details-v3__collapse-title">
|
||||
Flame Graph
|
||||
{traceData?.payload?.totalSpansCount &&
|
||||
traceData.payload.totalSpansCount > FLAMEGRAPH_SPAN_LIMIT && (
|
||||
@@ -323,15 +321,17 @@ function TraceDetailsV3(): JSX.Element {
|
||||
)}
|
||||
</span>
|
||||
{traceData?.payload?.totalSpansCount ? (
|
||||
<span className={styles.collapseCount}>
|
||||
<span className={styles.collapseCountItem}>
|
||||
<span className="trace-details-v3__collapse-count">
|
||||
<span className="trace-details-v3__collapse-count-item">
|
||||
<ChartNoAxesGantt size={13} />
|
||||
Spans: {traceData.payload.totalSpansCount}
|
||||
</span>
|
||||
<span
|
||||
className={cx(styles.collapseCountItem, {
|
||||
[styles.hasErrors]: traceData.payload.totalErrorSpansCount > 0,
|
||||
})}
|
||||
className={`trace-details-v3__collapse-count-item${
|
||||
traceData.payload.totalErrorSpansCount > 0
|
||||
? ' trace-details-v3__collapse-count-errors'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<TriangleAlert size={13} />
|
||||
Errors: {traceData.payload.totalErrorSpansCount ?? 0}
|
||||
@@ -360,9 +360,11 @@ function TraceDetailsV3(): JSX.Element {
|
||||
activeKey={activeKeys.filter((k) => k === 'waterfall')}
|
||||
onChange={(): void => handleCollapseChange('waterfall')}
|
||||
size="small"
|
||||
className={cx(styles.waterfallCollapse, {
|
||||
[styles.isDocked]: isWaterfallDocked,
|
||||
})}
|
||||
className={`trace-details-v3__waterfall-collapse${
|
||||
isWaterfallDocked
|
||||
? ' trace-details-v3__waterfall-collapse--docked'
|
||||
: ''
|
||||
}`}
|
||||
items={[
|
||||
{
|
||||
key: 'waterfall',
|
||||
@@ -373,7 +375,7 @@ function TraceDetailsV3(): JSX.Element {
|
||||
/>
|
||||
|
||||
{panelState.isOpen && isDocked && (
|
||||
<div className={styles.dockedSpanDetails}>
|
||||
<div className="trace-details-v3__docked-span-details">
|
||||
<SpanDetailsPanel
|
||||
panelState={panelState}
|
||||
selectedSpan={selectedSpan}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
29
tests/fixtures/querier.py
vendored
29
tests/fixtures/querier.py
vendored
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
285
tests/integration/tests/querier_json_body/02_non_body_fields.py
Normal file
285
tests/integration/tests/querier_json_body/02_non_body_fields.py
Normal 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)
|
||||
Reference in New Issue
Block a user