diff --git a/ecosystem-explorer/bun.lock b/ecosystem-explorer/bun.lock index f2ff5931..0ebb5615 100644 --- a/ecosystem-explorer/bun.lock +++ b/ecosystem-explorer/bun.lock @@ -8,8 +8,10 @@ "@grafana/faro-react": "^2.4.0", "@grafana/faro-web-tracing": "^2.4.0", "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "cmdk": "^1.1.1", "idb": "^8.0.3", "js-yaml": "^4.1.1", "lucide-react": "^1.0.0", @@ -221,14 +223,22 @@ "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="], "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], @@ -463,6 +473,8 @@ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], @@ -499,6 +511,8 @@ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], @@ -535,6 +549,8 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], "diff": ["diff@4.0.4", "", {}, "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ=="], @@ -613,6 +629,8 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -895,10 +913,16 @@ "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + "react-router": ["react-router@7.14.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw=="], "react-router-dom": ["react-router-dom@7.14.2", "", { "dependencies": { "react-router": "7.14.2" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ=="], + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], @@ -1015,6 +1039,10 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], diff --git a/ecosystem-explorer/package.json b/ecosystem-explorer/package.json index e71a4218..6ef79e35 100644 --- a/ecosystem-explorer/package.json +++ b/ecosystem-explorer/package.json @@ -28,8 +28,10 @@ "@grafana/faro-react": "^2.4.0", "@grafana/faro-web-tracing": "^2.4.0", "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "cmdk": "^1.1.1", "idb": "^8.0.3", "js-yaml": "^4.1.1", "lucide-react": "^1.0.0", diff --git a/ecosystem-explorer/src/components/ui/searchable-multi-select.test.tsx b/ecosystem-explorer/src/components/ui/searchable-multi-select.test.tsx new file mode 100644 index 00000000..5d24fd93 --- /dev/null +++ b/ecosystem-explorer/src/components/ui/searchable-multi-select.test.tsx @@ -0,0 +1,116 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { SearchableMultiSelect, SelectedChips } from "./searchable-multi-select"; + +describe("SearchableMultiSelect", () => { + const options = ["Apple", "Banana", "Cherry", "Date", "Elderberry"]; + const defaultProps = { + label: "Fruits", + placeholder: "Select fruits...", + options, + selected: [], + onChange: vi.fn(), + }; + + it("renders correctly with placeholder", () => { + render(); + expect(screen.getByText("Fruits")).toBeInTheDocument(); + expect(screen.getByText("Select fruits...")).toBeInTheDocument(); + }); + + it("shows number of selected items when selection exists", () => { + render(); + expect(screen.getByText("2 selected")).toBeInTheDocument(); + }); + + it("opens popover on click and displays options", async () => { + const user = userEvent.setup(); + render(); + + const trigger = screen.getByRole("button", { name: "Fruits" }); + await user.click(trigger); + + expect(screen.getByRole("dialog")).toBeInTheDocument(); + + for (const option of options) { + expect(screen.getByRole("option", { name: option })).toBeInTheDocument(); + } + }); + + it("filters options when typing in search input", async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole("button", { name: "Fruits" })); + + const searchInput = screen.getByPlaceholderText("Search..."); + await user.type(searchInput, "ba"); + + expect(screen.getByRole("option", { name: "Banana" })).toBeInTheDocument(); + expect(screen.queryByRole("option", { name: "Apple" })).not.toBeInTheDocument(); + }); + + it("calls onChange when an option is toggled", async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + render(); + + await user.click(screen.getByRole("button", { name: "Fruits" })); + await user.click(screen.getByRole("option", { name: "Banana" })); + + expect(onChange).toHaveBeenCalledWith(["Banana"]); + }); + + it("removes option if already selected", async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + render( + + ); + + await user.click(screen.getByRole("button", { name: "Fruits" })); + await user.click(screen.getByRole("option", { name: "Banana" })); + + expect(onChange).toHaveBeenCalledWith(["Apple"]); + }); +}); + +describe("SelectedChips", () => { + it("renders nothing if selected is empty", () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("renders chips for selected items", () => { + render(); + expect(screen.getByText("Apple")).toBeInTheDocument(); + expect(screen.getByText("Banana")).toBeInTheDocument(); + }); + + it("calls onRemove when a chip's remove button is clicked", async () => { + const user = userEvent.setup(); + const onRemove = vi.fn(); + render(); + + const removeAppleButton = screen.getByRole("button", { name: "Remove Apple" }); + await user.click(removeAppleButton); + + expect(onRemove).toHaveBeenCalledWith("Apple"); + }); +}); diff --git a/ecosystem-explorer/src/components/ui/searchable-multi-select.tsx b/ecosystem-explorer/src/components/ui/searchable-multi-select.tsx new file mode 100644 index 00000000..438b2904 --- /dev/null +++ b/ecosystem-explorer/src/components/ui/searchable-multi-select.tsx @@ -0,0 +1,157 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useState, useId, type ReactNode } from "react"; +import { Search, ChevronDown, Check, X } from "lucide-react"; +import * as Popover from "@radix-ui/react-popover"; +import { Command } from "cmdk"; + +interface SearchableMultiSelectProps { + label: string; + placeholder: string; + options: string[]; + selected: string[]; + onChange: (selected: string[]) => void; + renderOption?: (option: string) => ReactNode; + className?: string; +} + +export function SearchableMultiSelect({ + label, + placeholder, + options, + selected, + onChange, + renderOption, + className = "", +}: SearchableMultiSelectProps) { + const [isOpen, setIsOpen] = useState(false); + const triggerId = useId(); + + const toggleOption = (option: string) => { + const newSelected = selected.includes(option) + ? selected.filter((item) => item !== option) + : [...selected, option]; + onChange(newSelected); + }; + + return ( +
+ + + + + + + + + + +
+
+ + +
+
+ + + + No options found + + + {options.map((option) => ( + toggleOption(option)} + className={`hover:bg-primary/10 data-[selected=true]:bg-primary/10 data-[selected=true]:text-primary flex cursor-pointer items-center justify-between rounded-md px-3 py-2 text-sm transition-colors outline-none ${ + selected.includes(option) + ? "bg-primary/5 text-primary font-medium" + : "text-foreground" + }`} + > + {renderOption ? renderOption(option) : option} + {selected.includes(option) && } + + ))} + + +
+
+
+
+
+ ); +} + +export function SelectedChips({ + selected, + onRemove, + renderItem, + className = "", +}: { + selected: string[]; + onRemove: (item: string) => void; + renderItem?: (item: string) => ReactNode; + className?: string; +}) { + if (selected.length === 0) return null; + + return ( +
+ {selected.map((item) => ( + + {renderItem ? renderItem(item) : item} + + + ))} +
+ ); +} diff --git a/ecosystem-explorer/src/features/java-agent/components/instrumentation-badges.test.tsx b/ecosystem-explorer/src/features/java-agent/components/instrumentation-badges.test.tsx index 6775ca1b..2e17a881 100644 --- a/ecosystem-explorer/src/features/java-agent/components/instrumentation-badges.test.tsx +++ b/ecosystem-explorer/src/features/java-agent/components/instrumentation-badges.test.tsx @@ -38,6 +38,8 @@ const defaultFilters: FilterState = { search: "", telemetry: new Set(), target: new Set(), + semantic: [], + features: [], }; describe("TargetBadges", () => { @@ -73,6 +75,8 @@ describe("TargetBadges", () => { search: "", telemetry: new Set(), target: new Set(["javaagent"]), + semantic: [], + features: [], }; render( @@ -81,7 +85,13 @@ describe("TargetBadges", () => { }); it("highlights Library badge when library filter is active", () => { - const filters: FilterState = { search: "", telemetry: new Set(), target: new Set(["library"]) }; + const filters: FilterState = { + search: "", + telemetry: new Set(), + target: new Set(["library"]), + semantic: [], + features: [], + }; render( ); @@ -121,13 +131,25 @@ describe("TelemetryBadges", () => { }); it("highlights Spans badge when spans filter is active", () => { - const filters: FilterState = { search: "", telemetry: new Set(["spans"]), target: new Set() }; + const filters: FilterState = { + search: "", + telemetry: new Set(["spans"]), + target: new Set(), + semantic: [], + features: [], + }; render(); expect(screen.getByText("Spans").className).toContain(FILTER_STYLES.telemetry.spans.active); }); it("highlights Metrics badge when metrics filter is active", () => { - const filters: FilterState = { search: "", telemetry: new Set(["metrics"]), target: new Set() }; + const filters: FilterState = { + search: "", + telemetry: new Set(["metrics"]), + target: new Set(), + semantic: [], + features: [], + }; render(); expect(screen.getByText("Metrics").className).toContain(FILTER_STYLES.telemetry.metrics.active); }); diff --git a/ecosystem-explorer/src/features/java-agent/components/instrumentation-card.test.tsx b/ecosystem-explorer/src/features/java-agent/components/instrumentation-card.test.tsx index f07b6058..fec8cee6 100644 --- a/ecosystem-explorer/src/features/java-agent/components/instrumentation-card.test.tsx +++ b/ecosystem-explorer/src/features/java-agent/components/instrumentation-card.test.tsx @@ -164,6 +164,8 @@ describe("InstrumentationCard", () => { search: "", telemetry: new Set(), target: new Set(["javaagent"]), + semantic: [], + features: [], }; renderCard(instrumentation, activeFilters); @@ -181,6 +183,8 @@ describe("InstrumentationCard", () => { search: "", telemetry: new Set(), target: new Set(["library"]), + semantic: [], + features: [], }; renderCard(instrumentation, activeFilters); @@ -198,6 +202,8 @@ describe("InstrumentationCard", () => { search: "", telemetry: new Set(["spans"]), target: new Set(), + semantic: [], + features: [], }; renderCard(instrumentation, activeFilters); @@ -228,6 +234,8 @@ describe("InstrumentationCard", () => { search: "", telemetry: new Set(["metrics"]), target: new Set(), + semantic: [], + features: [], }; renderCard(instrumentation, activeFilters); diff --git a/ecosystem-explorer/src/features/java-agent/components/instrumentation-card.tsx b/ecosystem-explorer/src/features/java-agent/components/instrumentation-card.tsx index fc58eae4..4d78fa47 100644 --- a/ecosystem-explorer/src/features/java-agent/components/instrumentation-card.tsx +++ b/ecosystem-explorer/src/features/java-agent/components/instrumentation-card.tsx @@ -18,8 +18,13 @@ import type { InstrumentationData } from "@/types/javaagent"; import type { FilterState } from "./instrumentation-filter-bar"; import { getBadgeInfo } from "../utils/badge-info"; import { TargetBadges, TelemetryBadges } from "./instrumentation-badges"; -import { getInstrumentationDisplayName } from "../utils/format"; +import { + getInstrumentationDisplayName, + getSemanticConventionInfo, + getFeatureInfo, +} from "../utils/format"; import { renderWithInlineCode } from "@/lib/render-inline-code"; +import { GlowBadge } from "@/components/ui/glow-badge"; interface InstrumentationCardProps { instrumentation: InstrumentationData; @@ -60,6 +65,7 @@ export function InstrumentationCard({

{displayName}

+
@@ -69,8 +75,49 @@ export function InstrumentationCard({

)} -
- +
+ {instrumentation.semantic_conventions && + instrumentation.semantic_conventions.length > 0 && ( +
+ {instrumentation.semantic_conventions.map((s) => { + const isActive = + !activeFilters || + activeFilters.semantic.length === 0 || + activeFilters.semantic.includes(s); + const info = getSemanticConventionInfo(s); + return ( + + {info?.label ?? s} + + ); + })} +
+ )} + + {instrumentation.features && instrumentation.features.length > 0 && ( +
+ {instrumentation.features.map((f) => { + const isActive = + !activeFilters || + activeFilters.features.length === 0 || + activeFilters.features.includes(f); + const info = getFeatureInfo(f); + return ( + + {info?.label ?? f} + + ); + })} +
+ )}
diff --git a/ecosystem-explorer/src/features/java-agent/components/instrumentation-filter-bar.test.tsx b/ecosystem-explorer/src/features/java-agent/components/instrumentation-filter-bar.test.tsx index 5614a89c..3769ac36 100644 --- a/ecosystem-explorer/src/features/java-agent/components/instrumentation-filter-bar.test.tsx +++ b/ecosystem-explorer/src/features/java-agent/components/instrumentation-filter-bar.test.tsx @@ -24,18 +24,32 @@ describe("InstrumentationFilterBar", () => { search: "", telemetry: new Set(), target: new Set(), + semantic: [], + features: [], }; it("renders search input with correct placeholder", () => { const onFiltersChange = vi.fn(); - render(); + render( + + ); expect(screen.getByPlaceholderText("Search instrumentations...")).toBeInTheDocument(); }); it("calls onFiltersChange when search input changes", () => { const onFiltersChange = vi.fn(); - render(); + render( + + ); const input = screen.getByPlaceholderText("Search instrumentations..."); fireEvent.change(input, { target: { value: "http" } }); @@ -49,7 +63,13 @@ describe("InstrumentationFilterBar", () => { it("toggles spans telemetry filter on button click", async () => { const user = userEvent.setup(); const onFiltersChange = vi.fn(); - render(); + render( + + ); const spansButton = screen.getByRole("button", { name: "Spans" }); await user.click(spansButton); @@ -63,7 +83,13 @@ describe("InstrumentationFilterBar", () => { it("toggles metrics telemetry filter on button click", async () => { const user = userEvent.setup(); const onFiltersChange = vi.fn(); - render(); + render( + + ); const metricsButton = screen.getByRole("button", { name: "Metrics" }); await user.click(metricsButton); @@ -82,7 +108,13 @@ describe("InstrumentationFilterBar", () => { telemetry: new Set(["spans"]), }; - render(); + render( + + ); const spansButton = screen.getByRole("button", { name: "Spans" }); await user.click(spansButton); @@ -96,7 +128,13 @@ describe("InstrumentationFilterBar", () => { it("toggles java agent target filter on button click", async () => { const user = userEvent.setup(); const onFiltersChange = vi.fn(); - render(); + render( + + ); const javaAgentButton = screen.getByRole("button", { name: "Java Agent" }); await user.click(javaAgentButton); @@ -110,7 +148,13 @@ describe("InstrumentationFilterBar", () => { it("toggles standalone target filter on button click", async () => { const user = userEvent.setup(); const onFiltersChange = vi.fn(); - render(); + render( + + ); const standaloneButton = screen.getByRole("button", { name: "Standalone" }); await user.click(standaloneButton); @@ -126,9 +170,17 @@ describe("InstrumentationFilterBar", () => { search: "", telemetry: new Set(["spans", "metrics"]), target: new Set(["javaagent"]), + semantic: [], + features: [], }; - render(); + render( + + ); const spansButton = screen.getByRole("button", { name: "Spans" }); const metricsButton = screen.getByRole("button", { name: "Metrics" }); @@ -146,9 +198,17 @@ describe("InstrumentationFilterBar", () => { search: "", telemetry: new Set(["spans"]), target: new Set(["library"]), + semantic: [], + features: [], }; - render(); + render( + + ); const spansButton = screen.getByRole("button", { name: "Spans" }); const metricsButton = screen.getByRole("button", { name: "Metrics" }); diff --git a/ecosystem-explorer/src/features/java-agent/components/instrumentation-filter-bar.tsx b/ecosystem-explorer/src/features/java-agent/components/instrumentation-filter-bar.tsx index e52dab52..0cc1d5be 100644 --- a/ecosystem-explorer/src/features/java-agent/components/instrumentation-filter-bar.tsx +++ b/ecosystem-explorer/src/features/java-agent/components/instrumentation-filter-bar.tsx @@ -13,23 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { useMemo } from "react"; import { getTelemetryFilterClasses, getTargetFilterClasses } from "../styles/filter-styles"; import { Tooltip } from "@/components/ui/tooltip"; +import { SearchableMultiSelect, SelectedChips } from "@/components/ui/searchable-multi-select"; + +import { getSemanticConventionInfo, getFeatureInfo } from "../utils/format"; + +import type { InstrumentationData } from "@/types/javaagent"; export interface FilterState { search: string; telemetry: Set<"spans" | "metrics">; target: Set<"javaagent" | "library">; + semantic: string[]; + features: string[]; } interface InstrumentationFilterBarProps { filters: FilterState; onFiltersChange: (filters: FilterState) => void; + instrumentations: InstrumentationData[]; } export function InstrumentationFilterBar({ filters, onFiltersChange, + instrumentations, }: InstrumentationFilterBarProps) { const toggleTelemetry = (type: "spans" | "metrics") => { const newTelemetry = new Set(filters.telemetry); @@ -51,43 +61,81 @@ export function InstrumentationFilterBar({ onFiltersChange({ ...filters, target: newTarget }); }; + const semanticOptions = useMemo(() => { + const options = new Set(); + instrumentations.forEach((instr) => { + instr.semantic_conventions?.forEach((s) => options.add(s)); + }); + return Array.from(options).sort(); + }, [instrumentations]); + + const featureOptions = useMemo(() => { + const options = new Set(); + instrumentations.forEach((instr) => { + instr.features?.forEach((f) => options.add(f)); + }); + return Array.from(options).sort(); + }, [instrumentations]); + return ( -
+
{/* Ambient radial gradient background */} -
- -
+
+ +
+
+
-
-
- -
- onFiltersChange({ ...filters, search: e.target.value })} - className="border-border/60 bg-background/80 placeholder:text-muted-foreground/50 focus:border-primary/50 focus:ring-primary/20 w-full rounded-lg border px-4 py-2.5 text-sm backdrop-blur-sm transition-all duration-200 focus:ring-2 focus:outline-none" - /> +
+
+
+ +
+ onFiltersChange({ ...filters, search: e.target.value })} + className="border-border/60 bg-background/80 placeholder:text-muted-foreground/50 focus:border-primary/50 focus:ring-primary/20 flex h-[42px] w-full rounded-lg border px-4 py-2 text-sm backdrop-blur-sm transition-all duration-200 focus:ring-2 focus:outline-none" + /> +
+ + onFiltersChange({ ...filters, semantic: selected })} + renderOption={(s) => getSemanticConventionInfo(s)?.label ?? s} + /> + + onFiltersChange({ ...filters, features: selected })} + renderOption={(f) => getFeatureInfo(f)?.label ?? f} + />
@@ -147,6 +195,29 @@ export function InstrumentationFilterBar({
+ +
+ + onFiltersChange({ + ...filters, + semantic: filters.semantic.filter((s) => s !== item), + }) + } + renderItem={(s) => getSemanticConventionInfo(s)?.label ?? s} + /> + + onFiltersChange({ + ...filters, + features: filters.features.filter((f) => f !== item), + }) + } + renderItem={(f) => getFeatureInfo(f)?.label ?? f} + /> +
{/* Corner accent */} diff --git a/ecosystem-explorer/src/features/java-agent/components/instrumentation-group-card.test.tsx b/ecosystem-explorer/src/features/java-agent/components/instrumentation-group-card.test.tsx index c2874c01..410991e1 100644 --- a/ecosystem-explorer/src/features/java-agent/components/instrumentation-group-card.test.tsx +++ b/ecosystem-explorer/src/features/java-agent/components/instrumentation-group-card.test.tsx @@ -37,6 +37,8 @@ const defaultFilters: FilterState = { search: "", telemetry: new Set(), target: new Set(), + semantic: [], + features: [], }; describe("InstrumentationGroupCard", () => { diff --git a/ecosystem-explorer/src/features/java-agent/java-instrumentation-list-page.test.tsx b/ecosystem-explorer/src/features/java-agent/java-instrumentation-list-page.test.tsx index 26ab8eec..ed4ce011 100644 --- a/ecosystem-explorer/src/features/java-agent/java-instrumentation-list-page.test.tsx +++ b/ecosystem-explorer/src/features/java-agent/java-instrumentation-list-page.test.tsx @@ -55,6 +55,8 @@ describe("JavaInstrumentationListPage - Filtering", () => { has_javaagent: true, javaagent_target_versions: ["1.0.0"], telemetry: [{ when: "always", spans: [{ span_kind: "CLIENT" }] }], + semantic_conventions: ["http"], + features: ["stable"], }, { name: "jdbc", @@ -76,6 +78,7 @@ describe("JavaInstrumentationListPage - Filtering", () => { ], }, ], + semantic_conventions: ["db"], }, { name: "kafka-client", @@ -100,6 +103,8 @@ describe("JavaInstrumentationListPage - Filtering", () => { ], }, ], + semantic_conventions: ["messaging"], + features: ["stable"], }, { name: "spring-web", @@ -108,6 +113,7 @@ describe("JavaInstrumentationListPage - Filtering", () => { scope: { name: "spring" }, has_javaagent: true, javaagent_target_versions: ["1.0.0"], + features: ["experimental"], }, ]; @@ -355,4 +361,62 @@ describe("JavaInstrumentationListPage - Filtering", () => { expect(screen.getByText("Error loading instrumentations")).toBeInTheDocument(); expect(screen.getByText("Failed to load instrumentations")).toBeInTheDocument(); }); + + it("filters by semantic conventions (OR logic)", async () => { + const user = userEvent.setup(); + renderPage(); + + await waitFor(() => expect(screen.getByText("HTTP Client")).toBeInTheDocument()); + + const semanticButton = screen.getByRole("button", { name: /Semantic Conventions/i }); + await user.click(semanticButton); + + const httpOption = screen.getByRole("option", { name: "http" }); + await user.click(httpOption); + + expect(screen.getByText("HTTP Client")).toBeInTheDocument(); + expect(screen.queryByText("JDBC")).not.toBeInTheDocument(); + + const dbOption = screen.getByRole("option", { name: "db" }); + await user.click(dbOption); + + expect(screen.getByText("HTTP Client")).toBeInTheDocument(); + expect(screen.getByText("JDBC")).toBeInTheDocument(); + expect(screen.queryByText("Kafka Client")).not.toBeInTheDocument(); + }); + + it("filters by features (OR logic)", async () => { + const user = userEvent.setup(); + renderPage(); + + await waitFor(() => expect(screen.getByText("HTTP Client")).toBeInTheDocument()); + + const featuresButton = screen.getByRole("button", { name: /Features/i }); + await user.click(featuresButton); + + const experimentalOption = screen.getByRole("option", { name: "experimental" }); + await user.click(experimentalOption); + + expect(screen.getByText("Spring Web")).toBeInTheDocument(); + expect(screen.queryByText("HTTP Client")).not.toBeInTheDocument(); + }); + + it("combines filters from different categories (AND logic)", async () => { + const user = userEvent.setup(); + renderPage(); + + await waitFor(() => expect(screen.getByText("HTTP Client")).toBeInTheDocument()); + + const semanticButton = screen.getByRole("button", { name: /Semantic Conventions/i }); + await user.click(semanticButton); + await user.click(screen.getByRole("option", { name: "http" })); + + const featuresButton = screen.getByRole("button", { name: /Features/i }); + await user.click(featuresButton); + await user.click(screen.getByRole("option", { name: "stable" })); + + expect(screen.getByText("HTTP Client")).toBeInTheDocument(); + expect(screen.queryByText("Kafka Client")).not.toBeInTheDocument(); + expect(screen.queryByText("JDBC")).not.toBeInTheDocument(); + }); }); diff --git a/ecosystem-explorer/src/features/java-agent/java-instrumentation-list-page.tsx b/ecosystem-explorer/src/features/java-agent/java-instrumentation-list-page.tsx index dc090da2..4463f275 100644 --- a/ecosystem-explorer/src/features/java-agent/java-instrumentation-list-page.tsx +++ b/ecosystem-explorer/src/features/java-agent/java-instrumentation-list-page.tsx @@ -56,6 +56,8 @@ export function JavaInstrumentationListPage() { search: "", telemetry: new Set(), target: new Set(), + semantic: [], + features: [], }); const filteredInstrumentations = useMemo(() => { @@ -101,6 +103,18 @@ export function JavaInstrumentationListPage() { } } + if (filters.semantic.length > 0) { + const hasMatch = filters.semantic.some((s) => instr.semantic_conventions?.includes(s)); + if (!hasMatch) return false; + } + + if (filters.features.length > 0) { + const hasMatch = filters.features.some((f) => { + return instr.features?.includes(f); + }); + if (!hasMatch) return false; + } + return true; }); }, [instrumentations, filters]); @@ -200,7 +214,11 @@ export function JavaInstrumentationListPage() { )}
- +
diff --git a/ecosystem-explorer/src/features/java-agent/styles/filter-styles.ts b/ecosystem-explorer/src/features/java-agent/styles/filter-styles.ts index e339dc7d..7a13fda4 100644 --- a/ecosystem-explorer/src/features/java-agent/styles/filter-styles.ts +++ b/ecosystem-explorer/src/features/java-agent/styles/filter-styles.ts @@ -16,36 +16,40 @@ export const FILTER_STYLES = { telemetry: { spans: { - active: "bg-blue-500/30 border-blue-500 text-blue-700 dark:text-blue-300 shadow-sm", - inactive: "bg-blue-500/10 border-transparent text-blue-600 dark:text-blue-400", - hover: "hover:border-blue-500/50 hover:bg-blue-500/5", + active: + "bg-blue-500/40 border-blue-400 text-blue-100 shadow-[0_0_15px_rgba(59,130,246,0.3)] ring-1 ring-blue-400/50", + inactive: + "bg-blue-500/5 border-blue-500/20 text-blue-400/70 hover:border-blue-500/50 hover:bg-blue-500/10", }, metrics: { - active: "bg-green-500/30 border-green-500 text-green-700 dark:text-green-300 shadow-sm", - inactive: "bg-green-500/10 border-transparent text-green-600 dark:text-green-400", - hover: "hover:border-green-500/50 hover:bg-green-500/5", + active: + "bg-green-500/40 border-green-400 text-green-100 shadow-[0_0_15px_rgba(34,197,94,0.3)] ring-1 ring-green-400/50", + inactive: + "bg-green-500/5 border-green-500/20 text-green-400/70 hover:border-green-500/50 hover:bg-green-500/10", }, }, target: { javaagent: { - active: "bg-orange-500/30 border-orange-500 text-orange-700 dark:text-orange-300 shadow-sm", - inactive: "bg-orange-500/10 border-transparent text-orange-600 dark:text-orange-400", - hover: "hover:border-orange-500/50 hover:bg-orange-500/5", + active: + "bg-orange-500/40 border-orange-400 text-orange-100 shadow-[0_0_15px_rgba(249,115,22,0.3)] ring-1 ring-orange-400/50", + inactive: + "bg-orange-500/5 border-orange-500/20 text-orange-400/70 hover:border-orange-500/50 hover:bg-orange-500/10", }, library: { - active: "bg-purple-500/30 border-purple-500 text-purple-700 dark:text-purple-300 shadow-sm", - inactive: "bg-purple-500/10 border-transparent text-purple-600 dark:text-purple-400", - hover: "hover:border-purple-500/50 hover:bg-purple-500/5", + active: + "bg-purple-500/40 border-purple-400 text-purple-100 shadow-[0_0_15px_rgba(168,85,247,0.3)] ring-1 ring-purple-400/50", + inactive: + "bg-purple-500/5 border-purple-500/20 text-purple-400/70 hover:border-purple-500/50 hover:bg-purple-500/10", }, }, } as const; export function getTelemetryFilterClasses(type: "spans" | "metrics", isActive: boolean): string { const styles = FILTER_STYLES.telemetry[type]; - return isActive ? styles.active : `${styles.inactive} ${styles.hover}`; + return isActive ? styles.active : styles.inactive; } export function getTargetFilterClasses(type: "javaagent" | "library", isActive: boolean): string { const styles = FILTER_STYLES.target[type]; - return isActive ? styles.active : `${styles.inactive} ${styles.hover}`; + return isActive ? styles.active : styles.inactive; } diff --git a/ecosystem-explorer/src/index.css b/ecosystem-explorer/src/index.css index 13c74895..6c986906 100644 --- a/ecosystem-explorer/src/index.css +++ b/ecosystem-explorer/src/index.css @@ -101,3 +101,28 @@ html { .y-plain { color: hsl(var(--foreground-hsl)); } +.custom-scrollbar::-webkit-scrollbar { + width: 8px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: transparent; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: hsl(var(--border-hsl) / 0.8); + border-radius: 10px; + border: 2px solid transparent; + background-clip: padding-box; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: hsl(var(--primary-hsl) / 0.6); + border: 2px solid transparent; + background-clip: padding-box; +} + +.custom-scrollbar { + scrollbar-width: thin; + scrollbar-color: hsl(var(--border-hsl) / 0.8) transparent; +} diff --git a/ecosystem-explorer/src/test/setup.ts b/ecosystem-explorer/src/test/setup.ts index 2a98f066..e45cd17b 100644 --- a/ecosystem-explorer/src/test/setup.ts +++ b/ecosystem-explorer/src/test/setup.ts @@ -13,4 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { vi } from "vitest"; import "@testing-library/jest-dom"; + +class ResizeObserverMock { + constructor() {} + + observe() {} + unobserve() {} + disconnect() {} +} +globalThis.ResizeObserver = ResizeObserverMock as typeof ResizeObserver; +HTMLElement.prototype.scrollIntoView = vi.fn(); +HTMLElement.prototype.hasPointerCapture = vi.fn(); +HTMLElement.prototype.releasePointerCapture = vi.fn();