mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-22 20:25:38 +00:00
init
This commit is contained in:
committed by
Siddharth Gelera
parent
5e9cec38f0
commit
4077ce8eb2
151
package-lock.json
generated
151
package-lock.json
generated
@@ -3518,6 +3518,7 @@
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
|
||||
"integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.0",
|
||||
@@ -3535,6 +3536,7 @@
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
@@ -3551,6 +3553,7 @@
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@discoveryjs/json-ext": {
|
||||
@@ -3567,6 +3570,7 @@
|
||||
"version": "3.2.17",
|
||||
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.17.tgz",
|
||||
"integrity": "sha512-OcWImUI686w8LkghQj9R2ynZ2ME693Ek6L1SiaAgqGKzBaTIZw3fHDqN82Rcl+EU1Gm9EgkJ5KLIY/q5DCRbbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^5.0.0",
|
||||
@@ -3584,6 +3588,7 @@
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -3595,6 +3600,7 @@
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
@@ -3615,6 +3621,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
@@ -4312,6 +4319,7 @@
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
@@ -4329,6 +4337,7 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -4341,6 +4350,7 @@
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -4353,12 +4363,14 @@
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
@@ -4376,6 +4388,7 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
@@ -4391,6 +4404,7 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
@@ -5034,6 +5048,7 @@
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz",
|
||||
"integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
@@ -5049,6 +5064,7 @@
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -5066,6 +5082,7 @@
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"at-least-node": "^1.0.0",
|
||||
@@ -5081,6 +5098,7 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
@@ -5845,6 +5863,7 @@
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -7926,6 +7945,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
@@ -8019,6 +8039,7 @@
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/ms": "*"
|
||||
@@ -8056,6 +8077,7 @@
|
||||
"version": "9.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
|
||||
"integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -8248,6 +8270,7 @@
|
||||
"version": "0.7.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
|
||||
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
@@ -8980,6 +9003,7 @@
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -9003,6 +9027,7 @@
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
|
||||
"integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/abab": {
|
||||
@@ -9157,6 +9182,7 @@
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"ajv": "^6.9.1"
|
||||
@@ -9412,12 +9438,14 @@
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/async-exit-hook": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz",
|
||||
"integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@@ -9867,12 +9895,14 @@
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bluebird-lst": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz",
|
||||
"integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bluebird": "^3.5.5"
|
||||
@@ -9961,6 +9991,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -10238,6 +10269,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz",
|
||||
"integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
@@ -10808,6 +10840,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -10827,12 +10860,14 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz",
|
||||
"integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ci-info": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -11142,6 +11177,7 @@
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
|
||||
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -11157,6 +11193,7 @@
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz",
|
||||
"integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -11627,6 +11664,7 @@
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
@@ -12708,6 +12746,7 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ecc-jsbn": {
|
||||
@@ -12745,6 +12784,7 @@
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
||||
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"jake": "^10.8.5"
|
||||
@@ -13014,6 +13054,7 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
|
||||
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
@@ -14269,6 +14310,7 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^5.0.1"
|
||||
@@ -14278,6 +14320,7 @@
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
@@ -14544,6 +14587,7 @@
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
|
||||
"integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.0",
|
||||
@@ -14560,6 +14604,7 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -14714,6 +14759,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
@@ -15391,6 +15437,7 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
|
||||
"integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -15403,6 +15450,7 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
@@ -15415,6 +15463,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
@@ -16212,6 +16261,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
|
||||
"integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ci-info": "^3.2.0"
|
||||
@@ -16653,6 +16703,7 @@
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.4.tgz",
|
||||
"integrity": "sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
@@ -16665,6 +16716,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isobject": {
|
||||
@@ -16795,6 +16847,7 @@
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
@@ -16810,6 +16863,7 @@
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
|
||||
"integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"async": "^3.2.3",
|
||||
@@ -16828,6 +16882,7 @@
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -16838,6 +16893,7 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
@@ -16854,6 +16910,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
@@ -18494,6 +18551,7 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
||||
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/leven": {
|
||||
@@ -19157,6 +19215,7 @@
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
||||
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
@@ -19311,6 +19370,7 @@
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
@@ -19323,12 +19383,14 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"devOptional": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0",
|
||||
@@ -19342,6 +19404,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"devOptional": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
@@ -19943,6 +20006,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/pako": {
|
||||
@@ -20178,6 +20242,7 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -20194,6 +20259,7 @@
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
@@ -20210,12 +20276,14 @@
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/path-scurry/node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -20513,6 +20581,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
|
||||
"integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.8.8",
|
||||
@@ -21537,6 +21606,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
|
||||
"integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"err-code": "^2.0.2",
|
||||
@@ -22293,6 +22363,7 @@
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz",
|
||||
"integrity": "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"config-file-ts": "^0.2.4",
|
||||
@@ -22310,6 +22381,7 @@
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz",
|
||||
"integrity": "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"glob": "^10.3.10",
|
||||
@@ -22320,6 +22392,7 @@
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz",
|
||||
"integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -22329,12 +22402,14 @@
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz",
|
||||
"integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/read-config-file/node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
@@ -22355,6 +22430,7 @@
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
@@ -22370,6 +22446,7 @@
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -22379,6 +22456,7 @@
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -22848,6 +22926,7 @@
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
@@ -23280,6 +23359,7 @@
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
|
||||
"integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
|
||||
"dev": true,
|
||||
"license": "WTFPL OR ISC",
|
||||
"dependencies": {
|
||||
"truncate-utf8-bytes": "^1.0.0"
|
||||
@@ -24033,6 +24113,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
@@ -24045,6 +24126,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -24205,6 +24287,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
||||
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.3"
|
||||
@@ -24217,6 +24300,7 @@
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -24452,6 +24536,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
|
||||
"integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -24626,6 +24711,7 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -24676,6 +24762,7 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -25237,6 +25324,7 @@
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
@@ -25254,6 +25342,7 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -25263,6 +25352,7 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
@@ -25275,12 +25365,14 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"devOptional": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/temp-file": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz",
|
||||
"integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"async-exit-hook": "^2.0.1",
|
||||
@@ -25291,6 +25383,7 @@
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
@@ -25603,6 +25696,7 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
|
||||
"integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tmp": "^0.2.0"
|
||||
@@ -25694,6 +25788,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
||||
"integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
|
||||
"dev": true,
|
||||
"license": "WTFPL",
|
||||
"dependencies": {
|
||||
"utf8-byte-length": "^1.0.1"
|
||||
@@ -26177,6 +26272,7 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
|
||||
"integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==",
|
||||
"dev": true,
|
||||
"license": "(WTFPL OR MIT)"
|
||||
},
|
||||
"node_modules/util": {
|
||||
@@ -26513,6 +26609,7 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
@@ -26633,6 +26730,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
@@ -30411,7 +30509,6 @@
|
||||
"content-disposition": "^0.5.4",
|
||||
"decomment": "^0.9.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"electron-notarize": "^1.2.2",
|
||||
"electron-store": "^8.1.0",
|
||||
@@ -30435,6 +30532,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "~37.2.6",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-devtools-installer": "^4.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
@@ -30907,6 +31005,7 @@
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.2.1.tgz",
|
||||
"integrity": "sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
@@ -30921,6 +31020,7 @@
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"at-least-node": "^1.0.0",
|
||||
@@ -30936,6 +31036,7 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.5.tgz",
|
||||
"integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"compare-version": "^0.1.2",
|
||||
@@ -30957,6 +31058,7 @@
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz",
|
||||
"integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
@@ -30969,6 +31071,7 @@
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz",
|
||||
"integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.1",
|
||||
@@ -30987,6 +31090,7 @@
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -30997,6 +31101,7 @@
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"at-least-node": "^1.0.0",
|
||||
@@ -31012,6 +31117,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
@@ -31024,6 +31130,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz",
|
||||
"integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -31611,6 +31718,7 @@
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
@@ -31623,12 +31731,14 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz",
|
||||
"integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-electron/node_modules/app-builder-lib": {
|
||||
"version": "24.13.3",
|
||||
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.13.3.tgz",
|
||||
"integrity": "sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@develar/schema-utils": "~2.6.5",
|
||||
@@ -31682,6 +31792,7 @@
|
||||
"version": "24.13.1",
|
||||
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.13.1.tgz",
|
||||
"integrity": "sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/debug": "^4.1.6",
|
||||
@@ -31706,6 +31817,7 @@
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz",
|
||||
"integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
@@ -31719,6 +31831,7 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
||||
"integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tootallnate/once": "2",
|
||||
@@ -31733,6 +31846,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
@@ -31746,6 +31860,7 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
@@ -31762,6 +31877,7 @@
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -31779,6 +31895,7 @@
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz",
|
||||
"integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal": "^1.0.0",
|
||||
@@ -31789,6 +31906,7 @@
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -31799,6 +31917,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
@@ -31811,6 +31930,7 @@
|
||||
"version": "24.13.3",
|
||||
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz",
|
||||
"integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"app-builder-lib": "24.13.3",
|
||||
@@ -31828,6 +31948,7 @@
|
||||
"version": "24.13.3",
|
||||
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.13.3.tgz",
|
||||
"integrity": "sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"app-builder-lib": "24.13.3",
|
||||
@@ -31854,6 +31975,7 @@
|
||||
"version": "24.13.1",
|
||||
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz",
|
||||
"integrity": "sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "^9.0.11",
|
||||
@@ -31883,6 +32005,7 @@
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
@@ -31895,6 +32018,7 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-electron/node_modules/nanoid": {
|
||||
@@ -31919,6 +32043,7 @@
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -32202,7 +32327,8 @@
|
||||
"axios": "^1.9.0",
|
||||
"grpc-reflection-js": "^0.3.0",
|
||||
"is-ip": "^5.0.1",
|
||||
"tough-cookie": "^6.0.0"
|
||||
"tough-cookie": "^6.0.0",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.22.0",
|
||||
@@ -32261,6 +32387,27 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"packages/bruno-requests/node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/bruno-schema": {
|
||||
"name": "@usebruno/schema",
|
||||
"version": "0.7.0",
|
||||
|
||||
@@ -2,17 +2,17 @@ import React from 'react';
|
||||
|
||||
// UNARY - Single request, single response (Blue)
|
||||
export const IconGrpcUnary = ({ size = 18, strokeWidth = 1.5, className = '' }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
{/* Request arrow (top) - right */}
|
||||
<path d="M3 8h18" stroke="#3B82F6" strokeWidth={strokeWidth} />
|
||||
<path d="M18 5l3 3l-3 3" stroke="#3B82F6" strokeWidth={strokeWidth} />
|
||||
@@ -24,17 +24,17 @@ export const IconGrpcUnary = ({ size = 18, strokeWidth = 1.5, className = '' })
|
||||
|
||||
// CLIENT_STREAMING - Streaming request, single response (Purple)
|
||||
export const IconGrpcClientStreaming = ({ size = 18, strokeWidth = 1.5, className = '' }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
{/* Request arrow (top) - right with double heads */}
|
||||
<path d="M3 8h18" stroke="#8B5CF6" strokeWidth={strokeWidth} />
|
||||
<path d="M18 5l3 3l-3 3" stroke="#8B5CF6" strokeWidth={strokeWidth} />
|
||||
@@ -47,17 +47,17 @@ export const IconGrpcClientStreaming = ({ size = 18, strokeWidth = 1.5, classNam
|
||||
|
||||
// SERVER_STREAMING - Single request, streaming response (Green)
|
||||
export const IconGrpcServerStreaming = ({ size = 18, strokeWidth = 1.5, className = '' }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
{/* Request arrow (top) - right */}
|
||||
<path d="M3 8h18" stroke="#10B981" strokeWidth={strokeWidth} />
|
||||
<path d="M18 5l3 3l-3 3" stroke="#10B981" strokeWidth={strokeWidth} />
|
||||
@@ -70,17 +70,17 @@ export const IconGrpcServerStreaming = ({ size = 18, strokeWidth = 1.5, classNam
|
||||
|
||||
// BIDI_STREAMING - Streaming request, streaming response (Orange)
|
||||
export const IconGrpcBidiStreaming = ({ size = 18, strokeWidth = 1.5, className = '' }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
{/* Request arrow (top) - right with double heads */}
|
||||
<path d="M3 8h18" stroke="#F97316" strokeWidth={strokeWidth} />
|
||||
<path d="M18 5l3 3l-3 3" stroke="#F97316" strokeWidth={strokeWidth} />
|
||||
@@ -90,4 +90,30 @@ export const IconGrpcBidiStreaming = ({ size = 18, strokeWidth = 1.5, className
|
||||
<path d="M6 13l-3 3l3 3" stroke="#F97316" strokeWidth={strokeWidth} />
|
||||
<path d="M10 13l-3 3l3 3" stroke="#F97316" strokeWidth={strokeWidth} />
|
||||
</svg>
|
||||
);
|
||||
);
|
||||
|
||||
// WEBSOCKET - Bidirectional communication (Amber/Orange)
|
||||
// TODO: reaper move to it's own folder
|
||||
export const IconWebSocket = ({ size = 18, strokeWidth = 1.5, className = '' }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
{/* Bidirectional arrows representing WebSocket communication */}
|
||||
<path d="M3 8h18" stroke="#f59e0b" strokeWidth={strokeWidth} />
|
||||
<path d="M18 5l3 3l-3 3" stroke="#f59e0b" strokeWidth={strokeWidth} />
|
||||
<path d="M14 5l3 3l-3 3" stroke="#f59e0b" strokeWidth={strokeWidth} />
|
||||
<path d="M21 16h-18" stroke="#f59e0b" strokeWidth={strokeWidth} />
|
||||
<path d="M6 13l-3 3l3 3" stroke="#f59e0b" strokeWidth={strokeWidth} />
|
||||
<path d="M10 13l-3 3l3 3" stroke="#f59e0b" strokeWidth={strokeWidth} />
|
||||
{/* Connection indicator dots */}
|
||||
<circle cx="12" cy="12" r="2" fill="#f59e0b" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,11 @@ const BETA_FEATURES = [
|
||||
id: 'grpc',
|
||||
label: 'gRPC Support',
|
||||
description: 'Enable gRPC request support for making gRPC calls to services'
|
||||
},
|
||||
{
|
||||
id: 'websocket',
|
||||
label: 'Web Socket Support',
|
||||
description: 'Enable Web Socket request support for making realtime calls to services'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
|
||||
.content-indicator {
|
||||
color: ${(props) => props.theme.text}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.inherit-mode-text {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,85 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateRequestAuthMode } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
import StyledWrapper from '../../../Auth/AuthMode/StyledWrapper';
|
||||
|
||||
const WSAuthMode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
|
||||
const authModes = [
|
||||
{
|
||||
name: 'Basic Auth',
|
||||
mode: 'basic'
|
||||
},
|
||||
{
|
||||
name: 'Bearer Token',
|
||||
mode: 'bearer'
|
||||
},
|
||||
{
|
||||
name: 'API Key',
|
||||
mode: 'apikey'
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
mode: 'oauth2'
|
||||
},
|
||||
{
|
||||
name: 'Inherit',
|
||||
mode: 'inherit'
|
||||
},
|
||||
{
|
||||
name: 'No Auth',
|
||||
mode: 'none'
|
||||
}
|
||||
];
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center auth-mode-label select-none">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onModeChange = (value) => {
|
||||
dispatch(
|
||||
updateRequestAuthMode({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
mode: value
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onClickHandler = (mode) => {
|
||||
dropdownTippyRef?.current?.hide();
|
||||
onModeChange(mode);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
{authModes.map((authMode) => (
|
||||
<div
|
||||
key={authMode.mode}
|
||||
className="dropdown-item"
|
||||
onClick={() => onClickHandler(authMode.mode)}
|
||||
>
|
||||
{authMode.name}
|
||||
</div>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WSAuthMode;
|
||||
@@ -0,0 +1,125 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import WSAuthMode from './WSAuthMode';
|
||||
import BearerAuth from '../../Auth/BearerAuth';
|
||||
import BasicAuth from '../../Auth/BasicAuth';
|
||||
import ApiKeyAuth from '../../Auth/ApiKeyAuth';
|
||||
import OAuth2 from '../../Auth/OAuth2/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
import { getTreePathFromCollectionToItem } from 'utils/collections/index';
|
||||
import { updateRequestAuthMode, updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
// List of auth modes supported by gRPC
|
||||
const supportedAuthModes = ['basic', 'bearer', 'apikey', 'oauth2', 'none', 'inherit'];
|
||||
|
||||
const WSAuth = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
|
||||
const request = item.draft
|
||||
? get(item, 'draft.request', {})
|
||||
: get(item, 'request', {});
|
||||
|
||||
const save = () => {
|
||||
return saveRequest(item.uid, collection.uid);
|
||||
};
|
||||
|
||||
// Reset to 'none' if current auth mode is not supported
|
||||
useEffect(() => {
|
||||
if (authMode && !supportedAuthModes.includes(authMode)) {
|
||||
dispatch(
|
||||
updateRequestAuthMode({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
mode: 'none'
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [authMode, collection.uid, dispatch, item.uid]);
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: collectionAuth
|
||||
};
|
||||
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...requestTreePath].reverse()) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
name: i.name,
|
||||
auth: folderAuth
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return effectiveSource;
|
||||
};
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'basic': {
|
||||
return <BasicAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;
|
||||
}
|
||||
case 'bearer': {
|
||||
return <BearerAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;
|
||||
}
|
||||
case 'apikey': {
|
||||
return <ApiKeyAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;
|
||||
}
|
||||
case 'oauth2': {
|
||||
return <OAuth2 collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;
|
||||
}
|
||||
case 'inherit': {
|
||||
const source = getEffectiveAuthSource();
|
||||
|
||||
// Only show inherited auth if it's one of the supported types
|
||||
if (source && supportedAuthModes.includes(source.auth?.mode)) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
<div>Auth inherited from {source.name}: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
<div>Inherited auth not supported by gRPC. Using no auth instead.</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-1 overflow-y-scroll">
|
||||
<div className="flex flex-grow justify-start items-center">
|
||||
<WSAuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
{getAuthView()}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WSAuth;
|
||||
@@ -0,0 +1,121 @@
|
||||
import classnames from 'classnames';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import StatusDot from 'components/StatusDot/index';
|
||||
import { find } from 'lodash';
|
||||
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import { getPropertyFromDraftOrRequest } from 'utils/collections/index';
|
||||
import WsBody from '../WsBody/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import WSAuth from './WSAuth';
|
||||
|
||||
const WSRequestPane = ({ item, collection, handleRun }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
updateRequestPaneTab({
|
||||
uid: item.uid,
|
||||
requestPaneTab: tab
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'body': {
|
||||
return (
|
||||
<WsBody
|
||||
item={item}
|
||||
collection={collection}
|
||||
hideModeSelector={true}
|
||||
hidePrettifyButton={true}
|
||||
handleRun={handleRun}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} addHeaderText="Add Metadata" />;
|
||||
}
|
||||
case 'settings': {
|
||||
return <div className="flex w-full mt-10 justify-center text-neutral-400">TBD</div>;
|
||||
}
|
||||
case 'auth': {
|
||||
return <WSAuth item={item} collection={collection} />;
|
||||
}
|
||||
case 'docs': {
|
||||
return <Documentation item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!activeTabUid) {
|
||||
return <div>Something went wrong</div>;
|
||||
}
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === focusedTab.requestPaneTab
|
||||
});
|
||||
};
|
||||
|
||||
const isMultipleContentTab = ['script', 'vars', 'auth', 'docs'].includes(focusedTab.requestPaneTab);
|
||||
const headers = getPropertyFromDraftOrRequest(item, 'request.headers');
|
||||
const docs = getPropertyFromDraftOrRequest(item, 'request.docs');
|
||||
const auth = getPropertyFromDraftOrRequest(item, 'request.auth');
|
||||
|
||||
const activeHeadersLength = headers.filter((header) => header.enabled).length;
|
||||
|
||||
useEffect(() => {
|
||||
if (!focusedTab?.requestPaneTab) {
|
||||
selectTab('body');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
|
||||
Message
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Metadata
|
||||
{activeHeadersLength > 0 && <sup className="ml-[.125rem] font-medium">{activeHeadersLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
|
||||
Auth
|
||||
{auth.mode !== 'none' && <StatusDot type="default" />}
|
||||
</div>
|
||||
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
|
||||
Settings
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
{docs && docs.length > 0 && <StatusDot type="default" />}
|
||||
</div>
|
||||
</div>
|
||||
<section
|
||||
className={classnames('flex w-full flex-1 h-full', {
|
||||
'mt-2': !isMultipleContentTab
|
||||
})}
|
||||
>
|
||||
<HeightBoundContainer>{getTabPanel(focusedTab.requestPaneTab)}</HeightBoundContainer>
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WSRequestPane;
|
||||
@@ -0,0 +1,59 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
/* height: 100%; */
|
||||
position: relative;
|
||||
|
||||
.ws-message-header {
|
||||
.font-medium {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#grpc-messages-container {
|
||||
/* height: 100%; */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.add-message-btn-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-top: 8px;
|
||||
background: ${(props) => props.theme.bg || '#fff'};
|
||||
z-index: 15;
|
||||
border-top: 1px solid ${(props) => props.theme.border || 'rgba(0, 0, 0, 0.1)'};
|
||||
|
||||
.add-message-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
border-top: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
345
packages/bruno-app/src/components/RequestPane/WsBody/index.js
Normal file
345
packages/bruno-app/src/components/RequestPane/WsBody/index.js
Normal file
@@ -0,0 +1,345 @@
|
||||
import { get } from 'lodash';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { generateGrpcSampleMessage, sendWsRequest } from 'utils/network/index';
|
||||
import { IconChevronDown, IconChevronUp, IconPlus, IconRefresh, IconSend, IconTrash, IconWand } from '@tabler/icons';
|
||||
import CodeEditor from 'components/CodeEditor/index';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
import { applyEdits, format } from 'jsonc-parser';
|
||||
import toast from 'react-hot-toast';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const SingleWSMessage = ({
|
||||
message,
|
||||
item,
|
||||
collection,
|
||||
index,
|
||||
methodType,
|
||||
isCollapsed,
|
||||
onToggleCollapse,
|
||||
handleRun,
|
||||
canClientSendMultipleMessages
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { displayedTheme, theme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
const isConnectionActive = useSelector((state) => state.collections.activeConnections.includes(item.uid));
|
||||
|
||||
const canClientStream = methodType === 'client-streaming' || methodType === 'bidi-streaming';
|
||||
|
||||
const { name, content } = message;
|
||||
|
||||
const onEdit = (value) => {
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
|
||||
currentMessages[index] = {
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
content: value
|
||||
};
|
||||
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSend = async () => {
|
||||
try {
|
||||
await sendWsRequest(item, collection.uid, content);
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
}
|
||||
};
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const onRegenerateMessage = async () => {
|
||||
try {
|
||||
const methodPath = item.draft?.request?.method || item.request?.method;
|
||||
|
||||
if (!methodPath) {
|
||||
toastError(new Error('Method path not found in request'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the method metadata from the appropriate cache
|
||||
let methodMetadata = null;
|
||||
|
||||
const result = await generateGrpcSampleMessage(methodPath, content, {
|
||||
arraySize: 2,
|
||||
methodMetadata // Pass the method metadata to the function
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
|
||||
currentMessages[index] = {
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
content: result.message
|
||||
};
|
||||
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
|
||||
toast.success('Sample message generated successfully!');
|
||||
} else {
|
||||
toastError(new Error(result.error || 'Failed to generate sample message'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating sample message:', error);
|
||||
toastError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteMessage = () => {
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
|
||||
currentMessages.splice(index, 1);
|
||||
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onPrettify = () => {
|
||||
try {
|
||||
const edits = format(content, undefined, { tabSize: 2, insertSpaces: true });
|
||||
const prettyBodyJson = applyEdits(content, edits);
|
||||
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
currentMessages[index] = {
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
content: prettyBodyJson
|
||||
};
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
toastError(new Error('Unable to prettify. Invalid JSON format.'));
|
||||
}
|
||||
};
|
||||
|
||||
const getContainerHeight =
|
||||
canClientSendMultipleMessages && body.ws.length > 1 ? `${isCollapsed ? '' : 'h-80'}` : 'h-full';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col mb-3 border border-neutral-200 dark:border-neutral-800 rounded-md overflow-hidden ${getContainerHeight} relative`}
|
||||
>
|
||||
<div
|
||||
className="ws-message-header flex items-center justify-between px-3 py-2 bg-neutral-100 dark:bg-neutral-700 cursor-pointer"
|
||||
onClick={onToggleCollapse}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{isCollapsed ? (
|
||||
<IconChevronDown size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
) : (
|
||||
<IconChevronUp size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
)}
|
||||
<span className="font-medium text-sm">{`Message ${canClientStream ? index + 1 : ''}`}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<ToolHint text="Format JSON with proper indentation and spacing" toolhintId={`prettify-msg-${index}`}>
|
||||
<button
|
||||
onClick={onPrettify}
|
||||
className="p-1 rounded hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
|
||||
>
|
||||
<IconWand size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
<ToolHint text="Generate a new sample message based on schema" toolhintId={`regenerate-msg-${index}`}>
|
||||
<button
|
||||
onClick={onRegenerateMessage}
|
||||
className="p-1 rounded hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
|
||||
>
|
||||
<IconRefresh size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
{canClientStream && (
|
||||
<ToolHint
|
||||
text={isConnectionActive ? 'Send WS message' : 'Connection not active'}
|
||||
toolhintId={`send-msg-${index}`}
|
||||
>
|
||||
<button
|
||||
onClick={onSend}
|
||||
disabled={!isConnectionActive}
|
||||
className={`p-1 rounded ${
|
||||
isConnectionActive ? 'hover:bg-zinc-200 dark:hover:bg-zinc-600' : 'opacity-50 cursor-not-allowed'
|
||||
} transition-colors`}
|
||||
>
|
||||
<IconSend
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
className={`${
|
||||
isConnectionActive ? 'text-zinc-700 dark:text-zinc-300' : 'text-zinc-400 dark:text-zinc-500'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</ToolHint>
|
||||
)}
|
||||
|
||||
{index > 0 && (
|
||||
<ToolHint text="Delete this message" toolhintId={`delete-msg-${index}`}>
|
||||
<button
|
||||
onClick={onDeleteMessage}
|
||||
className="p-1 rounded hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
|
||||
>
|
||||
<IconTrash size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
</button>
|
||||
</ToolHint>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className={`flex ${body.ws.length === 1 || !canClientSendMultipleMessages ? 'h-full' : 'h-80'} relative`}>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
value={content}
|
||||
onEdit={onEdit}
|
||||
onRun={handleRun}
|
||||
onSave={onSave}
|
||||
mode="application/ld+json"
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WSBody = ({ item, collection, handleRun }) => {
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';
|
||||
const dispatch = useDispatch();
|
||||
const [collapsedMessages, setCollapsedMessages] = useState([]);
|
||||
const messagesContainerRef = useRef(null);
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
|
||||
const methodType = item.draft ? get(item, 'draft.request.methodType') : get(item, 'request.methodType');
|
||||
const canClientSendMultipleMessages = false;
|
||||
|
||||
// Auto-scroll to the latest message when messages are added
|
||||
useEffect(() => {
|
||||
if (messagesContainerRef.current && body?.ws?.length > 0) {
|
||||
const container = messagesContainerRef.current;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}, [body?.ws?.length]);
|
||||
|
||||
const toggleMessageCollapse = (index) => {
|
||||
setCollapsedMessages((prev) => {
|
||||
if (prev.includes(index)) {
|
||||
return prev.filter((i) => i !== index);
|
||||
} else {
|
||||
return [...prev, index];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addNewMessage = () => {
|
||||
const currentMessages = Array.isArray(body.ws) ? [...body.ws] : [];
|
||||
|
||||
currentMessages.push({
|
||||
name: `message ${currentMessages.length + 1}`,
|
||||
content: '{}'
|
||||
});
|
||||
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
if (!body?.ws || !Array.isArray(body.ws)) {
|
||||
return (
|
||||
<StyledWrapper isVerticalLayout={isVerticalLayout}>
|
||||
<div className="flex flex-col items-center justify-center py-8">
|
||||
<p className="text-zinc-500 dark:text-zinc-400 mb-4">No WebSocket messages available</p>
|
||||
<ToolHint text="Add the first message to your WebSocket request" toolhintId="add-first-msg">
|
||||
<button
|
||||
onClick={addNewMessage}
|
||||
className="flex items-center justify-center gap-2 py-2 px-4 rounded-md border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-700 hover:bg-neutral-200 dark:hover:bg-neutral-600 transition-colors"
|
||||
>
|
||||
<IconPlus size={16} strokeWidth={1.5} className="text-neutral-700 dark:text-neutral-300" />
|
||||
<span className="font-medium text-sm text-neutral-700 dark:text-neutral-300">Add First Message</span>
|
||||
</button>
|
||||
</ToolHint>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper isVerticalLayout={isVerticalLayout}>
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
id="grpc-messages-container"
|
||||
className={`flex-1 ${body.ws.length === 1 || !canClientSendMultipleMessages ? 'h-full' : 'overflow-y-auto'} ${
|
||||
canClientSendMultipleMessages && 'pb-16'
|
||||
}`}
|
||||
>
|
||||
{body.ws
|
||||
.filter((_, index) => canClientSendMultipleMessages || index === 0)
|
||||
.map((message, index) => (
|
||||
<SingleWSMessage
|
||||
key={index}
|
||||
message={message}
|
||||
item={item}
|
||||
collection={collection}
|
||||
index={index}
|
||||
methodType={methodType}
|
||||
isCollapsed={collapsedMessages.includes(index)}
|
||||
onToggleCollapse={() => toggleMessageCollapse(index)}
|
||||
handleRun={handleRun}
|
||||
canClientSendMultipleMessages={canClientSendMultipleMessages}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{canClientSendMultipleMessages && (
|
||||
<div className="add-message-btn-container">
|
||||
<ToolHint text="Add a new WebSocket message to the request" toolhintId="add-msg-fixed">
|
||||
<button
|
||||
onClick={addNewMessage}
|
||||
className="add-message-btn flex items-center justify-center gap-2 py-2 px-4 rounded-md border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-700 hover:bg-neutral-200 dark:hover:bg-neutral-600 transition-colors shadow-md"
|
||||
>
|
||||
<IconPlus size={16} strokeWidth={1.5} className="text-neutral-700 dark:text-neutral-300" />
|
||||
<span className="font-medium text-sm text-neutral-700 dark:text-neutral-300">Add Message</span>
|
||||
</button>
|
||||
</ToolHint>
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WSBody;
|
||||
@@ -0,0 +1,98 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
height: 2.3rem;
|
||||
|
||||
.input-container {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
|
||||
input {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.connection-status-strip {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
background-color: ${(props) => props.theme.colors.text.green};
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.infotip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.infotip:hover .infotip-text {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.infotip-text {
|
||||
visibility: hidden;
|
||||
width: auto;
|
||||
background-color: ${(props) => props.theme.requestTabs.active.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 34px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.infotip-text::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
border-width: 4px;
|
||||
border-style: solid;
|
||||
border-color: ${(props) => props.theme.requestTabs.active.bg} transparent transparent transparent;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
|
||||
.connection-controls {
|
||||
.infotip {
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.url.errorHoverBg};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,155 @@
|
||||
import { IconArrowRight, IconDeviceFloppy, IconPlugConnected, IconPlugConnectedX } from '@tabler/icons';
|
||||
import { IconWebSocket } from 'components/Icons/Grpc';
|
||||
import SingleLineEditor from 'components/SingleLineEditor/index';
|
||||
import { requestUrlChanged } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { getPropertyFromDraftOrRequest } from 'utils/collections';
|
||||
import { isMacOS } from 'utils/common/platform';
|
||||
import { closeWsConnection, connectWS, isWsConnectionActive } from 'utils/network/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WsQueryUrl = ({ item, collection, handleRun }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { theme, displayedTheme } = useTheme();
|
||||
const [isConnectionActive, setIsConnectionActive] = useState(false);
|
||||
// TODO: repear, better state for connecting
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const url = getPropertyFromDraftOrRequest(item, 'request.url');
|
||||
const headers = getPropertyFromDraftOrRequest(item, 'request.headers') || [];
|
||||
const saveShortcut = isMacOS() ? '⌘S' : 'Ctrl+S';
|
||||
|
||||
// Check connection status
|
||||
useEffect(() => {
|
||||
const checkConnectionStatus = async () => {
|
||||
try {
|
||||
const result = await isWsConnectionActive(item.uid);
|
||||
setIsConnectionActive(Boolean(result.isActive));
|
||||
} catch (error) {
|
||||
setIsConnectionActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkConnectionStatus();
|
||||
const interval = setInterval(checkConnectionStatus, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, [item.uid]);
|
||||
|
||||
const onUrlChange = (value) => {
|
||||
dispatch(
|
||||
requestUrlChanged({
|
||||
url: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleCloseConnection = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
closeWsConnection(item.uid)
|
||||
.then(() => {
|
||||
toast.success('WebSocket connection closed');
|
||||
setIsConnectionActive(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to close WebSocket connection:', err);
|
||||
toast.error('Failed to close WebSocket connection');
|
||||
});
|
||||
};
|
||||
|
||||
const handleRunClick = async (e) => {
|
||||
e.stopPropagation();
|
||||
if (!url) {
|
||||
toast.error('Please enter a valid WebSocket URL');
|
||||
return;
|
||||
}
|
||||
handleRun(e);
|
||||
};
|
||||
|
||||
const handleConnect = (e) => {
|
||||
connectWS(item, collection);
|
||||
};
|
||||
|
||||
const onSave = (finalValue) => {
|
||||
dispatch(saveRequest(item.uid, collection.uid));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center h-full">
|
||||
<div className="flex items-center input-container flex-1 w-full input-container h-full relative">
|
||||
<IconWebSocket size={20} strokeWidth={2} className="ml-2 mr-4" />
|
||||
<SingleLineEditor
|
||||
value={url}
|
||||
onSave={(finalValue) => onSave(finalValue)}
|
||||
onChange={onUrlChange}
|
||||
placeholder="ws://localhost:8080 or wss://example.com"
|
||||
className="w-full"
|
||||
theme={displayedTheme}
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer">
|
||||
<div
|
||||
className="infotip mr-3"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!item.draft) return;
|
||||
onSave();
|
||||
}}
|
||||
>
|
||||
<IconDeviceFloppy
|
||||
color={item.draft ? theme.colors.text.yellow : theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
className={`${item.draft ? 'cursor-pointer' : 'cursor-default'}`}
|
||||
/>
|
||||
<span className="infotip-text text-xs">
|
||||
Save <span className="shortcut">({saveShortcut})</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isConnectionActive && (
|
||||
<div className="connection-controls relative flex items-center h-full gap-3 mr-3">
|
||||
<div className="infotip" onClick={handleCloseConnection}>
|
||||
<IconPlugConnectedX
|
||||
color={theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<span className="infotip-text text-xs">Close Connection</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isConnectionActive && (
|
||||
<div className="connection-controls relative flex items-center h-full gap-3 mr-3">
|
||||
<div className="infotip" onClick={handleConnect}>
|
||||
<IconPlugConnected
|
||||
className="cursor-pointer"
|
||||
color={theme.requestTabPanel.url.icon}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
/>
|
||||
<span className="infotip-text text-xs">Close Connection</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="cursor-pointer" onClick={handleRunClick}>
|
||||
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isConnectionActive && <div className="connection-status-strip"></div>}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WsQueryUrl;
|
||||
@@ -29,6 +29,9 @@ import CollectionOverview from 'components/CollectionSettings/Overview';
|
||||
import RequestNotLoaded from './RequestNotLoaded';
|
||||
import RequestIsLoading from './RequestIsLoading';
|
||||
import FolderNotFound from './FolderNotFound';
|
||||
import WsQueryUrl from 'components/RequestPane/WsQueryUrl';
|
||||
import WSRequestPane from 'components/RequestPane/WSRequestPane';
|
||||
import WSResponsePane from 'components/ResponsePane/WsResponsePane';
|
||||
|
||||
const MIN_LEFT_PANE_WIDTH = 300;
|
||||
const MIN_RIGHT_PANE_WIDTH = 350;
|
||||
@@ -118,7 +121,7 @@ const RequestTabPanel = () => {
|
||||
if (newHeight < MIN_TOP_PANE_HEIGHT || newHeight > mainRect.height - MIN_BOTTOM_PANE_HEIGHT) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setTopPaneHeight(newHeight);
|
||||
} else {
|
||||
const newWidth = e.clientX - mainRect.left - dragOffset.current.x;
|
||||
@@ -185,6 +188,7 @@ const RequestTabPanel = () => {
|
||||
|
||||
const item = findItemInCollection(collection, activeTabUid);
|
||||
const isGrpcRequest = item?.type === 'grpc-request';
|
||||
const isWsRequest = item?.type === 'ws-request';
|
||||
|
||||
if (focusedTab.type === 'collection-runner') {
|
||||
return <RunnerResults collection={collection} />;
|
||||
@@ -229,6 +233,7 @@ const RequestTabPanel = () => {
|
||||
|
||||
const handleRun = async () => {
|
||||
const isGrpcRequest = item?.type === 'grpc-request';
|
||||
const isWsRequest = item?.type === 'ws-request';
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
|
||||
if (isGrpcRequest && !request.url) {
|
||||
@@ -241,6 +246,11 @@ const RequestTabPanel = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isWsRequest && !request.url) {
|
||||
toast.error('Please enter a valid WebSocket URL');
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(sendRequest(item, collection.uid)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
duration: 5000
|
||||
@@ -248,26 +258,40 @@ const RequestTabPanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: reaper, improve selection of panes
|
||||
return (
|
||||
<StyledWrapper className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''} ${isVerticalLayout ? 'vertical-layout' : ''}`}>
|
||||
<StyledWrapper
|
||||
className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''} ${
|
||||
isVerticalLayout ? 'vertical-layout' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="pt-4 pb-3 px-4">
|
||||
{isGrpcRequest ? (
|
||||
<GrpcQueryUrl item={item} collection={collection} handleRun={handleRun} />
|
||||
) : isWsRequest ? (
|
||||
<WsQueryUrl item={item} collection={collection} handleRun={handleRun} />
|
||||
) : (
|
||||
<QueryUrl item={item} collection={collection} handleRun={handleRun} />
|
||||
)}
|
||||
</div>
|
||||
<section ref={mainSectionRef} className={`main flex ${isVerticalLayout ? 'flex-col' : ''} flex-grow pb-4 relative overflow-auto`}>
|
||||
<section
|
||||
ref={mainSectionRef}
|
||||
className={`main flex ${isVerticalLayout ? 'flex-col' : ''} flex-grow pb-4 relative overflow-auto`}
|
||||
>
|
||||
<section className="request-pane">
|
||||
<div
|
||||
className="px-4 h-full"
|
||||
style={isVerticalLayout ? {
|
||||
height: `${Math.max(topPaneHeight, MIN_TOP_PANE_HEIGHT)}px`,
|
||||
minHeight: `${MIN_TOP_PANE_HEIGHT}px`,
|
||||
width: '100%'
|
||||
} : {
|
||||
width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`
|
||||
}}
|
||||
style={
|
||||
isVerticalLayout
|
||||
? {
|
||||
height: `${Math.max(topPaneHeight, MIN_TOP_PANE_HEIGHT)}px`,
|
||||
minHeight: `${MIN_TOP_PANE_HEIGHT}px`,
|
||||
width: '100%'
|
||||
}
|
||||
: {
|
||||
width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`
|
||||
}
|
||||
}
|
||||
>
|
||||
{item.type === 'graphql-request' ? (
|
||||
<GraphQLRequestPane
|
||||
@@ -279,12 +303,12 @@ const RequestTabPanel = () => {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.type === 'http-request' ? (
|
||||
<HttpRequestPane item={item} collection={collection} />
|
||||
) : null}
|
||||
{item.type === 'http-request' ? <HttpRequestPane item={item} collection={collection} /> : null}
|
||||
|
||||
{isGrpcRequest ? (
|
||||
<GrpcRequestPane item={item} collection={collection} handleRun={handleRun} />
|
||||
) : isWsRequest ? (
|
||||
<WSRequestPane item={item} collection={collection} handleRun={handleRun} />
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
@@ -295,18 +319,11 @@ const RequestTabPanel = () => {
|
||||
|
||||
<section className="response-pane flex-grow overflow-x-auto">
|
||||
{item.type === 'grpc-request' ? (
|
||||
<GrpcResponsePane
|
||||
item={item}
|
||||
collection={collection}
|
||||
|
||||
response={item.response}
|
||||
/>
|
||||
<GrpcResponsePane item={item} collection={collection} response={item.response} />
|
||||
) : item.type === 'ws-request' ? (
|
||||
<WSResponsePane item={item} collection={collection} response={item.response} />
|
||||
) : (
|
||||
<ResponsePane
|
||||
item={item}
|
||||
collection={collection}
|
||||
response={item.response}
|
||||
/>
|
||||
<ResponsePane item={item} collection={collection} response={item.response} />
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -45,42 +45,40 @@ const getEffectiveAuthSource = (collection, item) => {
|
||||
const Timeline = ({ collection, item }) => {
|
||||
// Get the effective auth source if auth mode is inherit
|
||||
const authSource = getEffectiveAuthSource(collection, item);
|
||||
const isGrpcRequest = item.type === 'grpc-request';
|
||||
|
||||
// TODO: reaper, might want to split it out and replicate the same timeline item as grpc
|
||||
const isGrpcRequest = item.type === 'grpc-request' || item.type === 'ws-request';
|
||||
|
||||
// Filter timeline entries based on new rules
|
||||
const combinedTimeline = ([...(collection?.timeline || [])]).filter(obj => {
|
||||
// Always show entries for this item
|
||||
if (obj.itemUid === item.uid) return true;
|
||||
const combinedTimeline = [...(collection?.timeline || [])]
|
||||
.filter((obj) => {
|
||||
// Always show entries for this item
|
||||
if (obj.itemUid === item.uid) return true;
|
||||
|
||||
// For OAuth2 entries, also show if auth is inherited
|
||||
if (obj.type === 'oauth2' && authSource) {
|
||||
if (authSource.type === 'folder' && obj.folderUid === authSource.uid) return true;
|
||||
if (authSource.type === 'collection' && !obj.folderUid) return true;
|
||||
}
|
||||
// For OAuth2 entries, also show if auth is inherited
|
||||
if (obj.type === 'oauth2' && authSource) {
|
||||
if (authSource.type === 'folder' && obj.folderUid === authSource.uid) return true;
|
||||
if (authSource.type === 'collection' && !obj.folderUid) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}).sort((a, b) => b.timestamp - a.timestamp)
|
||||
return false;
|
||||
})
|
||||
.sort((a, b) => b.timestamp - a.timestamp);
|
||||
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="pb-4 w-full flex flex-grow flex-col"
|
||||
>
|
||||
<StyledWrapper className="pb-4 w-full flex flex-grow flex-col">
|
||||
{/* Timeline container with scrollbar */}
|
||||
<div
|
||||
className="timeline-container"
|
||||
>
|
||||
<div className="timeline-container">
|
||||
{combinedTimeline.map((event, index) => {
|
||||
// Handle regular requests
|
||||
if (event.type === 'request') {
|
||||
|
||||
const { data, timestamp, eventType } = event;
|
||||
const { request, response, eventData = {}, timestamp: eventTimestamp = timestamp } = data;
|
||||
|
||||
const { request, response, eventData = {}, timestamp: eventTimestamp = timestamp } = data;
|
||||
|
||||
if (isGrpcRequest) {
|
||||
return (
|
||||
<div key={index} className="timeline-event mb-2">
|
||||
<GrpcTimelineItem
|
||||
timestamp={eventTimestamp}
|
||||
timestamp={eventTimestamp}
|
||||
request={request}
|
||||
response={response}
|
||||
eventType={eventType}
|
||||
@@ -91,7 +89,7 @@ const Timeline = ({ collection, item }) => {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Regular HTTP request
|
||||
return (
|
||||
<div key={index} className="timeline-event mb-2">
|
||||
@@ -119,7 +117,7 @@ const Timeline = ({ collection, item }) => {
|
||||
<div className="mt-2">
|
||||
{debugInfo && debugInfo.length > 0 ? (
|
||||
debugInfo.map((data, idx) => (
|
||||
<div className='ml-4' key={idx}>
|
||||
<div className="ml-4" key={idx}>
|
||||
<TimelineItem
|
||||
timestamp={timestamp}
|
||||
request={data?.request}
|
||||
@@ -137,7 +135,7 @@ const Timeline = ({ collection, item }) => {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
@@ -145,4 +143,4 @@ const Timeline = ({ collection, item }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Timeline;
|
||||
export default Timeline;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.bg};
|
||||
border-radius: 4px;
|
||||
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stream-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&.complete {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
&.cancelled {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
&.streaming {
|
||||
color: ${(props) => props.theme.colors.text.blue};
|
||||
}
|
||||
}
|
||||
|
||||
.message-counter {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,17 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
overflow-y: auto;
|
||||
|
||||
.ws-incoming {
|
||||
background: ${(props) => props.theme.table.striped};
|
||||
border-color: ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
.ws-outgoing {
|
||||
background: ${(props) => props.theme.bg};
|
||||
border-color: ${(props) => props.theme.table.border};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconArrowDown, IconArrowUp } from '@tabler/icons';
|
||||
|
||||
// Example message structure: { direction: 'incoming' | 'outgoing', timestamp, data }
|
||||
const WSMessagesList = ({ messages = [] }) => {
|
||||
if (!messages.length) {
|
||||
return <div className="p-4 text-gray-500">No messages yet.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="ws-messages-list flex flex-col gap-2 mt-4">
|
||||
{messages.map((msg, idx) => {
|
||||
const isIncoming = msg.direction === 'incoming';
|
||||
let content;
|
||||
try {
|
||||
content = typeof msg.message === 'string' ? msg.message : JSON.stringify(msg.message, null, 2);
|
||||
} catch {
|
||||
content = String(msg.message);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={classnames('ws-message flex flex-col rounded border p-2', {
|
||||
'ws-incoming': isIncoming,
|
||||
'ws-outgoing': !isIncoming
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span
|
||||
className={classnames(
|
||||
'font-semibold flex items-center gap-1',
|
||||
isIncoming ? 'text-blue-700' : 'text-green-700'
|
||||
)}
|
||||
>
|
||||
{isIncoming ? <IconArrowDown size={18} /> : <IconArrowUp size={18} />}
|
||||
{isIncoming ? 'RCVD' : 'SENT'}
|
||||
</span>
|
||||
{msg.timestamp && (
|
||||
<span className="text-xs text-gray-400">{new Date(msg.timestamp).toLocaleTimeString()}</span>
|
||||
)}
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap break-all text-sm">{content}</pre>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WSMessagesList;
|
||||
@@ -0,0 +1,96 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.bg};
|
||||
border-radius: 4px;
|
||||
|
||||
.CodeMirror {
|
||||
height: 100%;
|
||||
font-family: ${(props) => (props.font === 'default' ? 'monospace' : props.font)};
|
||||
font-size: ${(props) => (props.fontSize ? props.fontSize : '13px')};
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.card.bg};
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.plainGrid.hoverBg};
|
||||
}
|
||||
|
||||
&.open {
|
||||
background-color: ${(props) => props.theme.plainGrid.hoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
.error-header {
|
||||
background-color: ${(props) => (props.theme.bg === '#1e1e1e' ? 'rgba(185, 28, 28, 0.1)' : '#fee2e2')};
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stream-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&.complete {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
&.cancelled {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
&.streaming {
|
||||
color: ${(props) => props.theme.colors.text.blue};
|
||||
}
|
||||
}
|
||||
|
||||
.message-counter {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.response-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.response-message {
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-panel-background);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,126 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Accordion from 'components/Accordion';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { get } from 'lodash';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { formatISO9075 } from 'date-fns';
|
||||
import WSError from '../WSError';
|
||||
|
||||
const WSQueryResult = ({ item, collection }) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const [showErrorMessage, setShowErrorMessage] = useState(true);
|
||||
|
||||
const response = item.response || {};
|
||||
const responsesList = response?.responses || [];
|
||||
// Reverse the responses list to show the most recent at the top
|
||||
const reversedResponsesList = [...responsesList].reverse();
|
||||
const hasError = response.isError;
|
||||
const hasResponses = responsesList.length > 0;
|
||||
const errorMessage = response.error;
|
||||
|
||||
// Reset error visibility when a new response is received
|
||||
useEffect(() => {
|
||||
if (hasError) {
|
||||
setShowErrorMessage(true);
|
||||
}
|
||||
}, [response, hasError]);
|
||||
|
||||
// Format a timestamp to a human-readable format
|
||||
const formatTimestamp = (timestamp) => {
|
||||
if (!timestamp) return 'Unknown time';
|
||||
|
||||
try {
|
||||
const date = new Date(timestamp);
|
||||
return formatISO9075(date);
|
||||
} catch (e) {
|
||||
return 'Invalid time';
|
||||
}
|
||||
};
|
||||
|
||||
// Format JSON for display
|
||||
const formatJSON = (data) => {
|
||||
try {
|
||||
if (typeof data === 'string') {
|
||||
return JSON.stringify(JSON.parse(data), null, 2);
|
||||
}
|
||||
return JSON.stringify(data, null, 2);
|
||||
} catch (e) {
|
||||
return typeof data === 'string' ? data : JSON.stringify(data);
|
||||
}
|
||||
};
|
||||
|
||||
if (!hasResponses && !hasError) {
|
||||
return (
|
||||
<StyledWrapper className="w-full h-full relative flex flex-col">
|
||||
<div className="text-gray-500 dark:text-gray-400 p-4">No messages received</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full h-full relative flex flex-col mt-2">
|
||||
{hasError && showErrorMessage && <WSError error={errorMessage} onClose={() => setShowErrorMessage(false)} />}
|
||||
{hasResponses && (
|
||||
<div className={`overflow-y-auto ${responsesList.length === 1 ? 'flex-1' : ''}`}>
|
||||
{responsesList.length === 1 ? (
|
||||
// Single message - render directly without accordion
|
||||
<div className="h-full">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
theme={displayedTheme}
|
||||
value={formatJSON(reversedResponsesList[0])}
|
||||
mode="application/json"
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// Multiple messages - use accordion
|
||||
<Accordion defaultIndex={0}>
|
||||
{reversedResponsesList.map((response, index) => {
|
||||
// Calculate the original response number (for display purposes)
|
||||
const originalIndex = responsesList.length - index - 1;
|
||||
|
||||
return (
|
||||
<Accordion.Item key={originalIndex} index={index}>
|
||||
<Accordion.Header index={index} style={{ padding: '8px 12px', minHeight: '40px' }}>
|
||||
<div className="flex justify-between w-full">
|
||||
<div className="font-medium">
|
||||
Response {originalIndex + 1} {index === 0 ? '(Latest)' : ''}
|
||||
</div>
|
||||
</div>
|
||||
</Accordion.Header>
|
||||
<Accordion.Content index={index} style={{ padding: '0px' }}>
|
||||
<div className="h-60">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
theme={displayedTheme}
|
||||
value={formatJSON(response)}
|
||||
mode="application/json"
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{hasError && !hasResponses && !showErrorMessage && (
|
||||
<div className="text-gray-500 dark:text-gray-400 p-4">
|
||||
No messages received. A server error occurred but has been dismissed.
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WSQueryResult;
|
||||
@@ -0,0 +1,31 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead {
|
||||
color: #777777;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&.value {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr:nth-child(odd) {
|
||||
background-color: ${(props) => props.theme.table.striped};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WSResponseHeaders = ({ metadata }) => {
|
||||
// Ensure headers is an array
|
||||
const metadataArray = Array.isArray(metadata) ? metadata : [];
|
||||
|
||||
return (
|
||||
<StyledWrapper className="pb-4 w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{metadataArray && metadataArray.length ? (
|
||||
metadataArray.map((metadata, index) => (
|
||||
<tr key={index}>
|
||||
<td className="key">{metadata.name}</td>
|
||||
<td className="value">{metadata.value}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="2" className="text-center py-4 text-gray-500">
|
||||
No metadata received
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WSResponseHeaders;
|
||||
@@ -0,0 +1,22 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.text-ok {
|
||||
color: ${(props) => props.theme.requestTabPanel.responseOk};
|
||||
}
|
||||
|
||||
&.text-pending {
|
||||
color: ${(props) => props.theme.requestTabPanel.responsePending};
|
||||
}
|
||||
|
||||
&.text-error {
|
||||
color: ${(props) => props.theme.requestTabPanel.responseError};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,20 @@
|
||||
const wsStatusCodePhraseMap = {
|
||||
1000: 'NORMAL_CLOSURE',
|
||||
1001: 'GOING_AWAY',
|
||||
1002: 'PROTOCOL_ERROR',
|
||||
1003: 'UNSUPPORTED_DATA',
|
||||
1004: 'RESERVED',
|
||||
1005: 'NO_STATUS_RECEIVED',
|
||||
1006: 'ABNORMAL_CLOSURE',
|
||||
1007: 'INVALID_FRAME_PAYLOAD_DATA',
|
||||
1008: 'POLICY_VIOLATION',
|
||||
1009: 'MESSAGE_TOO_BIG',
|
||||
1010: 'MANDATORY_EXTENSION',
|
||||
1011: 'INTERNAL_ERROR',
|
||||
1012: 'SERVICE_RESTART',
|
||||
1013: 'TRY_AGAIN_LATER',
|
||||
1014: 'BAD_GATEWAY',
|
||||
1015: 'TLS_HANDSHAKE'
|
||||
};
|
||||
|
||||
export default wsStatusCodePhraseMap;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import wsStatusCodePhraseMap from './get-ws-status-code-phrase';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WSStatusCode = ({ status, text }) => {
|
||||
// gRPC status codes: 0 is success, anything else is an error
|
||||
const getTabClassname = (status) => {
|
||||
const isPending = text === 'PENDING' || text === 'STREAMING';
|
||||
return classnames('ml-2', {
|
||||
'text-ok': parseInt(status) === 0,
|
||||
'text-pending': isPending,
|
||||
'text-error': parseInt(status) > 0 && !isPending
|
||||
});
|
||||
};
|
||||
|
||||
const statusText = text || wsStatusCodePhraseMap[status]
|
||||
|
||||
return (
|
||||
<StyledWrapper className={getTabClassname(status)}>
|
||||
{Number.isInteger(status) ? <div className="mr-1">{status}</div> : null}
|
||||
{statusText && <div>{statusText}</div>}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WSStatusCode;
|
||||
@@ -0,0 +1,44 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
border-left: 4px solid ${(props) => props.theme.colors.text.danger};
|
||||
border-top: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
max-height: 200px;
|
||||
min-height: 70px;
|
||||
overflow-y: auto;
|
||||
background-color: ${(props) => (props.theme.bg === '#1e1e1e' ? 'rgba(40, 40, 40, 0.5)' : 'rgba(250, 250, 250, 0.9)')};
|
||||
|
||||
.close-button {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.375rem;
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-family: monospace;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.25rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { IconX } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WSError = ({ error, onClose }) => {
|
||||
if (!error) return null;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-4 mb-2">
|
||||
<div className="flex items-start gap-3 px-4 py-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="error-title">WebSocket Server Error</div>
|
||||
<div className="error-message">{typeof error === 'string' ? error : JSON.stringify(error, null, 2)}</div>
|
||||
</div>
|
||||
<div className="close-button flex-shrink-0 cursor-pointer" onClick={onClose}>
|
||||
<IconX size={16} strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WSError;
|
||||
@@ -0,0 +1,150 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import classnames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import Overlay from '../Overlay';
|
||||
import Placeholder from '../Placeholder';
|
||||
import WSResponseHeaders from './WSResponseHeaders';
|
||||
import WSStatusCode from './WSStatusCode';
|
||||
import ResponseTime from '../ResponseTime/index';
|
||||
import Timeline from '../Timeline';
|
||||
import ClearTimeline from '../ClearTimeline';
|
||||
import ResponseClear from '../ResponseClear';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ResponseLayoutToggle from '../ResponseLayoutToggle';
|
||||
import Tab from 'components/Tab';
|
||||
import WSMessagesList from './WSMessagesList';
|
||||
|
||||
const WSResponsePane = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const isLoading = ['queued', 'sending'].includes(item.requestState);
|
||||
|
||||
const requestTimeline = [...(collection?.timeline || [])].filter((obj) => {
|
||||
if (obj.itemUid === item.uid) return true;
|
||||
});
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
updateResponsePaneTab({
|
||||
uid: item.uid,
|
||||
responsePaneTab: tab
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const response = item.response || {};
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'response': {
|
||||
return <WSMessagesList messages={response.responses || []} />;
|
||||
}
|
||||
case 'headers': {
|
||||
return <WSResponseHeaders metadata={response.metadata} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline collection={collection} item={item} />;
|
||||
}
|
||||
default: {
|
||||
return <div>404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading && !item.response) {
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<Overlay item={item} collection={collection} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (!item.response && !requestTimeline?.length) {
|
||||
return (
|
||||
<StyledWrapper className="flex h-full relative">
|
||||
<Placeholder />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (!activeTabUid) {
|
||||
return <div>Something went wrong</div>;
|
||||
}
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (!focusedTab || !focusedTab.uid || !focusedTab.responsePaneTab) {
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
const tabConfig = [
|
||||
{
|
||||
name: 'response',
|
||||
label: 'Messages',
|
||||
count: Array.isArray(response.responses) ? response.responses.length : 0
|
||||
},
|
||||
{
|
||||
name: 'headers',
|
||||
label: 'Metadata',
|
||||
count: Array.isArray(response.metadata) ? response.metadata.length : 0
|
||||
},
|
||||
{
|
||||
name: 'timeline',
|
||||
label: 'Timeline'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center pl-3 pr-4 tabs" role="tablist">
|
||||
{tabConfig.map((tab) => (
|
||||
<Tab
|
||||
key={tab.name}
|
||||
name={tab.name}
|
||||
label={tab.label}
|
||||
isActive={focusedTab.responsePaneTab === tab.name}
|
||||
onClick={selectTab}
|
||||
count={tab.count}
|
||||
/>
|
||||
))}
|
||||
{!isLoading ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
{focusedTab?.responsePaneTab === 'timeline' ? (
|
||||
<>
|
||||
<ResponseLayoutToggle />
|
||||
<ClearTimeline item={item} collection={collection} />
|
||||
</>
|
||||
) : item?.response ? (
|
||||
<>
|
||||
<ResponseLayoutToggle />
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<WSStatusCode
|
||||
status={response.statusCode}
|
||||
text={response.statusText}
|
||||
details={response.statusDescription}
|
||||
/>
|
||||
<ResponseTime duration={response.duration} />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className={`flex flex-col flex-grow pl-3 pr-4 h-0 ${focusedTab.responsePaneTab === 'response' ? '' : 'mt-4'}`}
|
||||
>
|
||||
{isLoading ? <Overlay item={item} collection={collection} /> : null}
|
||||
{!item?.response ? (
|
||||
focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? (
|
||||
<Timeline collection={collection} item={item} />
|
||||
) : null
|
||||
) : (
|
||||
<>{getTabPanel(focusedTab.responsePaneTab)}</>
|
||||
)}
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WSResponsePane;
|
||||
@@ -37,6 +37,9 @@ const Wrapper = styled.div`
|
||||
.method-grpc {
|
||||
color: ${(props) => props.theme.request.grpc};
|
||||
}
|
||||
.method-ws {
|
||||
color: ${(props) => props.theme.request.ws};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,33 +1,52 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
|
||||
const getMethodFlags = (item) => ({
|
||||
isGrpc: item.type === 'grpc-request',
|
||||
isWS: item.type === 'ws-request'
|
||||
});
|
||||
|
||||
const getMethodText = (item, { isGrpc, isWS }) =>
|
||||
isGrpc
|
||||
? 'grpc'
|
||||
: isWS
|
||||
? 'ws'
|
||||
: item.request.method.length > 5
|
||||
? item.request.method.substring(0, 3)
|
||||
: item.request.method;
|
||||
|
||||
const getClassname = (method = '', { isGrpc, isWS }) => {
|
||||
method = method.toLocaleLowerCase();
|
||||
return classnames('mr-1', {
|
||||
'method-get': method === 'get',
|
||||
'method-post': method === 'post',
|
||||
'method-put': method === 'put',
|
||||
'method-delete': method === 'delete',
|
||||
'method-patch': method === 'patch',
|
||||
'method-head': method === 'head',
|
||||
'method-options': method === 'options',
|
||||
'method-grpc': isGrpc,
|
||||
'method-ws': isWS
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const RequestMethod = ({ item }) => {
|
||||
if (!['http-request', 'graphql-request', 'grpc-request'].includes(item.type)) {
|
||||
if (!['http-request', 'graphql-request', 'grpc-request', 'ws-request'].includes(item.type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isGrpc = item.type === 'grpc-request';
|
||||
|
||||
const getClassname = (method = '') => {
|
||||
method = method.toLocaleLowerCase();
|
||||
return classnames('mr-1', {
|
||||
'method-get': method === 'get',
|
||||
'method-post': method === 'post',
|
||||
'method-put': method === 'put',
|
||||
'method-delete': method === 'delete',
|
||||
'method-patch': method === 'patch',
|
||||
'method-head': method === 'head',
|
||||
'method-options': method === 'options',
|
||||
'method-grpc': isGrpc,
|
||||
});
|
||||
};
|
||||
const flags = getMethodFlags(item);
|
||||
const methodText = getMethodText(item, flags);
|
||||
const className = getClassname(item.request.method, flags);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className={getClassname(item.request.method)}>
|
||||
<div className={className}>
|
||||
<span className="uppercase">
|
||||
{isGrpc ? 'grpc' : item.request.method.length > 5 ? item.request.method.substring(0, 3) : item.request.method}
|
||||
{methodText}
|
||||
</span>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { uuid } from 'utils/common';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { newEphemeralHttpRequest } from 'providers/ReduxStore/slices/collections';
|
||||
import { newHttpRequest, newGrpcRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { newHttpRequest, newGrpcRequest, newWsRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
@@ -29,6 +29,7 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
|
||||
|
||||
const storedTheme = useTheme();
|
||||
const isGrpcEnabled = useBetaFeature(BETA_FEATURES.GRPC);
|
||||
const isWsEnabled = useBetaFeature(BETA_FEATURES.WEBSOCKET);
|
||||
|
||||
const collection = useSelector((state) => state.collections.collections?.find((c) => c.uid === collectionUid));
|
||||
const {
|
||||
@@ -99,6 +100,14 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
|
||||
return 'grpc-request';
|
||||
}
|
||||
|
||||
if (collectionPresets.requestType === 'ws') {
|
||||
// If WebSocket is disabled in beta features, fall back to http-request
|
||||
if (!isWsEnabled) {
|
||||
return 'http-request';
|
||||
}
|
||||
return 'ws-request';
|
||||
}
|
||||
|
||||
return 'http-request';
|
||||
};
|
||||
|
||||
@@ -146,6 +155,7 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
const isGrpcRequest = values.requestType === 'grpc-request';
|
||||
const isWsRequest = values.requestType === 'ws-request';
|
||||
|
||||
if (isGrpcRequest) {
|
||||
dispatch(
|
||||
@@ -165,6 +175,23 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
|
||||
// will need to handle import from grpcurl command when we support it, now it is just for creating new requests
|
||||
} else if (isWsRequest) {
|
||||
dispatch(
|
||||
newWsRequest({
|
||||
requestName: values.requestName,
|
||||
requestMethod: values.requestMethod,
|
||||
filename: values.filename,
|
||||
requestType: values.requestType,
|
||||
requestUrl: values.requestUrl,
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item ? item.uid : null
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
toast.success('New request created!');
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
} else if (isEphemeral) {
|
||||
const uid = uuid();
|
||||
dispatch(
|
||||
@@ -309,69 +336,80 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
|
||||
Type
|
||||
</label>
|
||||
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="http-request"
|
||||
className="cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
value="http-request"
|
||||
checked={formik.values.requestType === 'http-request'}
|
||||
/>
|
||||
<label htmlFor="http-request" className="ml-1 cursor-pointer select-none">
|
||||
HTTP
|
||||
</label>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="http-request"
|
||||
name="requestType"
|
||||
value="http-request"
|
||||
checked={formik.values.requestType === 'http-request'}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<label htmlFor="http-request" className="ml-1 cursor-pointer select-none">
|
||||
HTTP Request
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<input
|
||||
id="graphql-request"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={(event) => {
|
||||
formik.setFieldValue('requestMethod', 'POST');
|
||||
formik.handleChange(event);
|
||||
}}
|
||||
value="graphql-request"
|
||||
checked={formik.values.requestType === 'graphql-request'}
|
||||
/>
|
||||
<label htmlFor="graphql-request" className="ml-1 cursor-pointer select-none">
|
||||
GraphQL
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="graphql-request"
|
||||
name="requestType"
|
||||
value="graphql-request"
|
||||
checked={formik.values.requestType === 'graphql-request'}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<label htmlFor="graphql-request" className="ml-1 cursor-pointer select-none">
|
||||
GraphQL Request
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{isGrpcEnabled && (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id="grpc-request"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
id="grpc-request"
|
||||
name="requestType"
|
||||
onChange={(event) => {
|
||||
formik.setFieldValue('requestMethod', 'POST');
|
||||
formik.handleChange(event);
|
||||
}}
|
||||
value="grpc-request"
|
||||
checked={formik.values.requestType === 'grpc-request'}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<label htmlFor="grpc-request" className="ml-1 cursor-pointer select-none">
|
||||
gRPC
|
||||
gRPC Request
|
||||
</label>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
id="from-curl"
|
||||
className="cursor-pointer ml-auto"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
value="from-curl"
|
||||
checked={formik.values.requestType === 'from-curl'}
|
||||
/>
|
||||
{isWsEnabled && (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="ws-request"
|
||||
name="requestType"
|
||||
value="ws-request"
|
||||
checked={formik.values.requestType === 'ws-request'}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<label htmlFor="ws-request" className="ml-1 cursor-pointer select-none">
|
||||
WebSocket Request
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label htmlFor="from-curl" className="ml-1 cursor-pointer select-none">
|
||||
From cURL
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="from-curl"
|
||||
name="requestType"
|
||||
value="from-curl"
|
||||
checked={formik.values.requestType === 'from-curl'}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<label htmlFor="from-curl" className="ml-1 cursor-pointer select-none">
|
||||
From cURL
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
@@ -462,7 +500,7 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
|
||||
URL
|
||||
</label>
|
||||
<div className="flex items-center mt-2 ">
|
||||
{formik.values.requestType !== 'grpc-request' ? (
|
||||
{!['grpc-request', 'ws-request'].includes(formik.values.requestType) ? (
|
||||
<div className="flex items-center h-full method-selector-container">
|
||||
<HttpMethodSelector
|
||||
method={formik.values.requestMethod}
|
||||
|
||||
@@ -12,6 +12,8 @@ import 'codemirror/theme/material.css';
|
||||
import 'codemirror/theme/monokai.css';
|
||||
import 'codemirror/addon/scroll/simplescrollbars.css';
|
||||
import Devtools from 'components/Devtools';
|
||||
import useGrpcEventListeners from 'utils/network/grpc-event-listeners';
|
||||
import useWsEventListeners from 'utils/network/ws-event-listeners';
|
||||
|
||||
require('codemirror/mode/javascript/javascript');
|
||||
require('codemirror/mode/xml/xml');
|
||||
@@ -52,38 +54,42 @@ export default function Main() {
|
||||
const isConsoleOpen = useSelector((state) => state.logs.isConsoleOpen);
|
||||
const mainSectionRef = useRef(null);
|
||||
|
||||
// Initialize event listeners
|
||||
useGrpcEventListeners();
|
||||
useWsEventListeners();
|
||||
|
||||
const className = classnames({
|
||||
'is-dragging': isDragging
|
||||
});
|
||||
|
||||
return (
|
||||
// <ErrorCapture>
|
||||
<div className="flex flex-col h-screen max-h-screen overflow-hidden">
|
||||
<div
|
||||
ref={mainSectionRef}
|
||||
className="flex-1 min-h-0 flex"
|
||||
style={{
|
||||
height: isConsoleOpen ? `calc(100vh - 22px - ${isConsoleOpen ? '300px' : '0px'})` : 'calc(100vh - 22px)'
|
||||
}}
|
||||
>
|
||||
<StyledWrapper className={className} style={{ height: '100%', zIndex: 1 }}>
|
||||
<Sidebar />
|
||||
<section className="flex flex-grow flex-col overflow-hidden">
|
||||
{showHomePage ? (
|
||||
<Welcome />
|
||||
) : (
|
||||
<>
|
||||
<RequestTabs />
|
||||
<RequestTabPanel key={activeTabUid} />
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
</div>
|
||||
|
||||
<Devtools mainSectionRef={mainSectionRef} />
|
||||
<StatusBar />
|
||||
<div className="flex flex-col h-screen max-h-screen overflow-hidden">
|
||||
<div
|
||||
ref={mainSectionRef}
|
||||
className="flex-1 min-h-0 flex"
|
||||
style={{
|
||||
height: isConsoleOpen ? `calc(100vh - 22px - ${isConsoleOpen ? '300px' : '0px'})` : 'calc(100vh - 22px)'
|
||||
}}
|
||||
>
|
||||
<StyledWrapper className={className} style={{ height: '100%', zIndex: 1 }}>
|
||||
<Sidebar />
|
||||
<section className="flex flex-grow flex-col overflow-hidden">
|
||||
{showHomePage ? (
|
||||
<Welcome />
|
||||
) : (
|
||||
<>
|
||||
<RequestTabs />
|
||||
<RequestTabPanel key={activeTabUid} />
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
</div>
|
||||
|
||||
<Devtools mainSectionRef={mainSectionRef} />
|
||||
<StatusBar />
|
||||
</div>
|
||||
// </ErrorCapture>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ const initialState = {
|
||||
codeFont: 'default'
|
||||
},
|
||||
beta: {
|
||||
grpc: false
|
||||
grpc: false,
|
||||
websocket: false
|
||||
}
|
||||
},
|
||||
generateCode: {
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
transformRequestToSaveToFilesystem
|
||||
} from 'utils/collections';
|
||||
import { uuid, waitForNextTick } from 'utils/common';
|
||||
import { cancelNetworkRequest, sendGrpcRequest, sendNetworkRequest } from 'utils/network/index';
|
||||
import { cancelNetworkRequest, sendGrpcRequest, sendNetworkRequest, sendWsRequest } from 'utils/network/index';
|
||||
import { callIpc } from 'utils/common/ipc';
|
||||
|
||||
import {
|
||||
@@ -282,12 +282,19 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);
|
||||
const isGrpcRequest = itemCopy.type === 'grpc-request';
|
||||
const isWsRequest = itemCopy.type === 'ws-request';
|
||||
if (isGrpcRequest) {
|
||||
sendGrpcRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables)
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
} else if (isWsRequest) {
|
||||
sendWsRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables)
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
} else {
|
||||
sendNetworkRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables)
|
||||
.then((response) => {
|
||||
@@ -1038,6 +1045,66 @@ export const newGrpcRequest = (params) => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const newWsRequest = (params) => (dispatch, getState) => {
|
||||
const { requestName, requestMethod, filename, requestUrl, collectionUid, body, auth, headers, itemUid } = params;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
const item = {
|
||||
uid: uuid(),
|
||||
name: requestName,
|
||||
filename,
|
||||
type: 'ws-request',
|
||||
headers: headers ?? [],
|
||||
request: {
|
||||
url: requestUrl,
|
||||
method: requestMethod,
|
||||
body: body ?? {
|
||||
mode: 'ws',
|
||||
ws: [
|
||||
{
|
||||
name: 'message 1',
|
||||
content: '{}'
|
||||
}
|
||||
]
|
||||
},
|
||||
auth: auth ?? {
|
||||
mode: 'inherit'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resolvedFilename = resolveRequestFilename(filename);
|
||||
const fullName = path.join(collection.pathname, resolvedFilename);
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
// Set the seq field for WebSocket requests
|
||||
const items = filter(collection.items, (i) => isItemAFolder(i) || isItemARequest(i));
|
||||
item.seq = items.length + 1;
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:new-request', fullName, item)
|
||||
.then(() => {
|
||||
// task middleware will track this and open the new request in a new tab once request is created
|
||||
dispatch(
|
||||
insertTaskIntoQueue({
|
||||
uid: uuid(),
|
||||
type: 'OPEN_REQUEST',
|
||||
collectionUid,
|
||||
itemPathname: fullName
|
||||
})
|
||||
);
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const loadGrpcMethodsFromReflection = (item, collectionUid, url) => async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
@@ -44,6 +44,26 @@ const grpcStatusCodes = {
|
||||
16: 'UNAUTHENTICATED'
|
||||
};
|
||||
|
||||
// WebSocket status code meanings
|
||||
const wsStatusCodes = {
|
||||
1000: 'NORMAL_CLOSURE',
|
||||
1001: 'GOING_AWAY',
|
||||
1002: 'PROTOCOL_ERROR',
|
||||
1003: 'UNSUPPORTED_DATA',
|
||||
1004: 'RESERVED',
|
||||
1005: 'NO_STATUS_RECEIVED',
|
||||
1006: 'ABNORMAL_CLOSURE',
|
||||
1007: 'INVALID_FRAME_PAYLOAD_DATA',
|
||||
1008: 'POLICY_VIOLATION',
|
||||
1009: 'MESSAGE_TOO_BIG',
|
||||
1010: 'MANDATORY_EXTENSION',
|
||||
1011: 'INTERNAL_ERROR',
|
||||
1012: 'SERVICE_RESTART',
|
||||
1013: 'TRY_AGAIN_LATER',
|
||||
1014: 'BAD_GATEWAY',
|
||||
1015: 'TLS_HANDSHAKE'
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
collections: [],
|
||||
collectionSortOrder: 'default',
|
||||
@@ -62,8 +82,24 @@ const initiatedGrpcResponse = {
|
||||
isError: false,
|
||||
duration: 0,
|
||||
responses: [],
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
const initiatedWsResponse = {
|
||||
status: 'PENDING',
|
||||
statusText: 'PENDING',
|
||||
statusCode: 0,
|
||||
headers: [],
|
||||
body: '',
|
||||
size: 0,
|
||||
duration: 0,
|
||||
responses: [],
|
||||
isError: false,
|
||||
error: null,
|
||||
errorDetails: null,
|
||||
metadata: [],
|
||||
trailers: []
|
||||
};
|
||||
|
||||
export const collectionsSlice = createSlice({
|
||||
name: 'collections',
|
||||
@@ -136,7 +172,7 @@ export const collectionsSlice = createSlice({
|
||||
},
|
||||
sortCollections: (state, action) => {
|
||||
state.collectionSortOrder = action.payload.order;
|
||||
const collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
||||
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
|
||||
switch (action.payload.order) {
|
||||
case 'default':
|
||||
state.collections = state.collections.sort((a, b) => a.importedAt - b.importedAt);
|
||||
@@ -329,7 +365,7 @@ export const collectionsSlice = createSlice({
|
||||
},
|
||||
responseReceived: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
if (item) {
|
||||
@@ -341,15 +377,16 @@ export const collectionsSlice = createSlice({
|
||||
if (!collection.timeline) {
|
||||
collection.timeline = [];
|
||||
}
|
||||
|
||||
|
||||
// Ensure timestamp is a number (milliseconds since epoch)
|
||||
const timestamp = item?.requestSent?.timestamp instanceof Date
|
||||
? item.requestSent.timestamp.getTime()
|
||||
: item?.requestSent?.timestamp || Date.now();
|
||||
const timestamp =
|
||||
item?.requestSent?.timestamp instanceof Date
|
||||
? item.requestSent.timestamp.getTime()
|
||||
: item?.requestSent?.timestamp || Date.now();
|
||||
|
||||
// Append the new timeline entry with numeric timestamp
|
||||
collection.timeline.push({
|
||||
type: "request",
|
||||
type: 'request',
|
||||
collectionUid: collection.uid,
|
||||
folderUid: null,
|
||||
itemUid: item.uid,
|
||||
@@ -357,7 +394,7 @@ export const collectionsSlice = createSlice({
|
||||
data: {
|
||||
request: item.requestSent || item.request,
|
||||
response: action.payload.response,
|
||||
timestamp: timestamp,
|
||||
timestamp: timestamp
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -367,7 +404,7 @@ export const collectionsSlice = createSlice({
|
||||
const { itemUid, collectionUid, eventType, eventData } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) return;
|
||||
|
||||
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (!item) return;
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
@@ -387,7 +424,7 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
|
||||
collection.timeline.push({
|
||||
type: "request",
|
||||
type: 'request',
|
||||
eventType: eventType, // Add the specific gRPC event type
|
||||
collectionUid: collection.uid,
|
||||
folderUid: null,
|
||||
@@ -396,36 +433,34 @@ export const collectionsSlice = createSlice({
|
||||
data: {
|
||||
request: eventData || item.requestSent || item.request,
|
||||
timestamp: Date.now(),
|
||||
eventData: eventData,
|
||||
eventData: eventData
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
grpcResponseReceived: (state, action) => {
|
||||
const { itemUid, collectionUid, eventType, eventData } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
|
||||
if (!collection) return;
|
||||
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
|
||||
if (!item) return;
|
||||
|
||||
|
||||
// Get current response state or create initial state
|
||||
const currentResponse = item.response || initiatedGrpcResponse
|
||||
const currentResponse = item.response || initiatedGrpcResponse;
|
||||
const timestamp = item?.requestSent?.timestamp;
|
||||
let updatedResponse = { ...currentResponse, duration: Date.now() - (timestamp || Date.now()) };
|
||||
|
||||
|
||||
// Process based on event type
|
||||
switch (eventType) {
|
||||
case 'response':
|
||||
const { error, res } = eventData;
|
||||
|
||||
|
||||
// Handle error if present
|
||||
if (error) {
|
||||
const errorCode = error.code || 2; // Default to UNKNOWN if no code
|
||||
|
||||
|
||||
updatedResponse.error = error.details || 'gRPC error occurred';
|
||||
updatedResponse.statusCode = errorCode;
|
||||
updatedResponse.statusText = grpcStatusCodes[errorCode] || 'UNKNOWN';
|
||||
@@ -434,72 +469,73 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
|
||||
// Add response to list
|
||||
updatedResponse.responses = res
|
||||
? [...(currentResponse?.responses || []), res]
|
||||
updatedResponse.responses = res
|
||||
? [...(currentResponse?.responses || []), res]
|
||||
: [...(currentResponse?.responses || [])];
|
||||
break;
|
||||
|
||||
|
||||
case 'metadata':
|
||||
updatedResponse.headers = eventData.metadata;
|
||||
updatedResponse.metadata = eventData.metadata;
|
||||
break;
|
||||
|
||||
|
||||
case 'status':
|
||||
// Extract status info
|
||||
const statusCode = eventData.status?.code;
|
||||
const statusDetails = eventData.status?.details;
|
||||
const statusMetadata = eventData.status?.metadata;
|
||||
|
||||
|
||||
// Set status based on actual code and details
|
||||
updatedResponse.statusCode = statusCode;
|
||||
updatedResponse.statusText = grpcStatusCodes[statusCode] || 'UNKNOWN';
|
||||
updatedResponse.statusDescription = statusDetails;
|
||||
updatedResponse.statusDetails = eventData.status;
|
||||
|
||||
|
||||
// Store trailers (status metadata)
|
||||
if (statusMetadata) {
|
||||
updatedResponse.trailers = statusMetadata;
|
||||
}
|
||||
|
||||
|
||||
// Handle error status (non-zero code)
|
||||
if (statusCode !== 0) {
|
||||
updatedResponse.isError = true;
|
||||
updatedResponse.error = statusDetails || `gRPC error with code ${statusCode} (${updatedResponse.statusText})`;
|
||||
updatedResponse.error =
|
||||
statusDetails || `gRPC error with code ${statusCode} (${updatedResponse.statusText})`;
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
|
||||
|
||||
case 'error':
|
||||
// Extract error details
|
||||
const errorCode = eventData.error?.code || 2; // Default to UNKNOWN if no code
|
||||
const errorDetails = eventData.error?.details || eventData.error?.message;
|
||||
const errorMetadata = eventData.error?.metadata;
|
||||
|
||||
|
||||
updatedResponse.isError = true;
|
||||
updatedResponse.error = errorDetails || 'Unknown gRPC error';
|
||||
updatedResponse.statusCode = errorCode;
|
||||
updatedResponse.statusText = grpcStatusCodes[errorCode] || 'UNKNOWN';
|
||||
updatedResponse.statusDescription = errorDetails;
|
||||
|
||||
|
||||
// Store error metadata as trailers if present
|
||||
if (errorMetadata) {
|
||||
updatedResponse.trailers = errorMetadata;
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
|
||||
|
||||
case 'end':
|
||||
state.activeConnections = state.activeConnections.filter(id => id !== itemUid);
|
||||
state.activeConnections = state.activeConnections.filter((id) => id !== itemUid);
|
||||
break;
|
||||
|
||||
|
||||
case 'cancel':
|
||||
updatedResponse.statusCode = 1; // CANCELLED
|
||||
updatedResponse.statusText = 'CANCELLED';
|
||||
updatedResponse.statusDescription = 'Stream cancelled by client or server';
|
||||
state.activeConnections = state.activeConnections.filter(id => id !== itemUid);
|
||||
state.activeConnections = state.activeConnections.filter((id) => id !== itemUid);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
item.requestState = 'received';
|
||||
item.response = updatedResponse;
|
||||
|
||||
@@ -510,7 +546,7 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
// Append the new timeline entry with specific gRPC event type
|
||||
collection.timeline.push({
|
||||
type: "request",
|
||||
type: 'request',
|
||||
eventType: eventType, // Add the specific gRPC event type
|
||||
collectionUid: collection.uid,
|
||||
folderUid: null,
|
||||
@@ -520,7 +556,7 @@ export const collectionsSlice = createSlice({
|
||||
request: item.requestSent || item.request,
|
||||
response: updatedResponse,
|
||||
eventData: eventData, // Store the original event data
|
||||
timestamp: Date.now(),
|
||||
timestamp: Date.now()
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -547,7 +583,7 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
if (collection) {
|
||||
if (itemUid) {
|
||||
collection.timeline = collection?.timeline?.filter(t => t?.itemUid !== itemUid);
|
||||
collection.timeline = collection?.timeline?.filter((t) => t?.itemUid !== itemUid);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -761,7 +797,7 @@ export const collectionsSlice = createSlice({
|
||||
case 'ntlm':
|
||||
item.draft.request.auth.mode = 'ntlm';
|
||||
item.draft.request.auth.ntlm = action.payload.content;
|
||||
break;
|
||||
break;
|
||||
case 'oauth2':
|
||||
item.draft.request.auth.mode = 'oauth2';
|
||||
item.draft.request.auth.oauth2 = action.payload.content;
|
||||
@@ -774,6 +810,10 @@ export const collectionsSlice = createSlice({
|
||||
item.draft.request.auth.mode = 'apikey';
|
||||
item.draft.request.auth.apikey = action.payload.content;
|
||||
break;
|
||||
case 'ws':
|
||||
item.draft.request.auth.mode = 'ws';
|
||||
item.draft.request.auth.ws = action.payload.content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -816,7 +856,7 @@ export const collectionsSlice = createSlice({
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
const existingOtherParams = item.draft.request.params?.filter(p => p.type !== 'query') || [];
|
||||
const existingOtherParams = item.draft.request.params?.filter((p) => p.type !== 'query') || [];
|
||||
const newQueryParams = map(params, ({ name = '', value = '', enabled = true }) => ({
|
||||
uid: uuid(),
|
||||
name,
|
||||
@@ -830,9 +870,7 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
// Update the request URL to reflect the new query params
|
||||
const parts = splitOnFirst(item.draft.request.url, '?');
|
||||
const query = stringifyQueryParams(
|
||||
filter(item.draft.request.params, (p) => p.enabled && p.type === 'query')
|
||||
);
|
||||
const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query'));
|
||||
|
||||
// If there are enabled query params, append them to the URL
|
||||
if (query && query.length) {
|
||||
@@ -860,13 +898,13 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
const queryParams = params.filter((param) => param.type === 'query');
|
||||
const pathParams = params.filter((param) => param.type === 'path');
|
||||
|
||||
|
||||
// Reorder only query params based on updateReorderedItem
|
||||
const reorderedQueryParams = updateReorderedItem.map((uid) => {
|
||||
return queryParams.find((param) => param.uid === uid);
|
||||
});
|
||||
item.draft.request.params = [...reorderedQueryParams, ...pathParams];
|
||||
|
||||
|
||||
// Update request URL
|
||||
const parts = splitOnFirst(item.draft.request.url, '?');
|
||||
const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query'));
|
||||
@@ -1063,7 +1101,7 @@ export const collectionsSlice = createSlice({
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.headers = map(action.payload.headers, ({name = '', value = '', enabled = true}) => ({
|
||||
item.draft.request.headers = map(action.payload.headers, ({ name = '', value = '', enabled = true }) => ({
|
||||
uid: uuid(),
|
||||
name: name,
|
||||
value: value,
|
||||
@@ -1079,7 +1117,7 @@ export const collectionsSlice = createSlice({
|
||||
return;
|
||||
}
|
||||
|
||||
collection.root.request.headers = map(headers, ({name = '', value = '', enabled = true}) => ({
|
||||
collection.root.request.headers = map(headers, ({ name = '', value = '', enabled = true }) => ({
|
||||
uid: uuid(),
|
||||
name: name,
|
||||
value: value,
|
||||
@@ -1099,8 +1137,8 @@ export const collectionsSlice = createSlice({
|
||||
if (!folder || !isItemAFolder(folder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
folder.root.request.headers = map(headers, ({name = '', value = '', enabled = true}) => ({
|
||||
|
||||
folder.root.request.headers = map(headers, ({ name = '', value = '', enabled = true }) => ({
|
||||
uid: uuid(),
|
||||
name: name,
|
||||
value: value,
|
||||
@@ -1274,16 +1312,16 @@ export const collectionsSlice = createSlice({
|
||||
},
|
||||
addFile: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.body.file = item.draft.request.body.file || [];
|
||||
|
||||
|
||||
item.draft.request.body.file.push({
|
||||
uid: uuid(),
|
||||
filePath: '',
|
||||
@@ -1295,23 +1333,23 @@ export const collectionsSlice = createSlice({
|
||||
},
|
||||
updateFile: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
|
||||
|
||||
const param = find(item.draft.request.body.file, (p) => p.uid === action.payload.param.uid);
|
||||
|
||||
|
||||
if (param) {
|
||||
const contentType = mime.contentType(path.extname(action.payload.param.filePath));
|
||||
param.filePath = action.payload.param.filePath;
|
||||
param.contentType = action.payload.param.contentType || contentType || '';
|
||||
param.selected = action.payload.param.selected;
|
||||
|
||||
|
||||
item.draft.request.body.file = item.draft.request.body.file.map((p) => {
|
||||
p.selected = p.uid === param.uid;
|
||||
return p;
|
||||
@@ -1322,20 +1360,17 @@ export const collectionsSlice = createSlice({
|
||||
},
|
||||
deleteFile: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
|
||||
item.draft.request.body.file = filter(
|
||||
item.draft.request.body.file,
|
||||
(p) => p.uid !== action.payload.paramUid
|
||||
);
|
||||
|
||||
|
||||
item.draft.request.body.file = filter(item.draft.request.body.file, (p) => p.uid !== action.payload.paramUid);
|
||||
|
||||
if (item.draft.request.body.file.length > 0) {
|
||||
item.draft.request.body.file[0].selected = true;
|
||||
}
|
||||
@@ -1381,7 +1416,7 @@ export const collectionsSlice = createSlice({
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
|
||||
|
||||
switch (item.draft.request.body.mode) {
|
||||
case 'json': {
|
||||
item.draft.request.body.json = action.payload.content;
|
||||
@@ -1415,6 +1450,10 @@ export const collectionsSlice = createSlice({
|
||||
item.draft.request.body.grpc = action.payload.content;
|
||||
break;
|
||||
}
|
||||
case 'ws': {
|
||||
item.draft.request.body.ws = action.payload.content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1514,7 +1553,7 @@ export const collectionsSlice = createSlice({
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
@@ -1710,17 +1749,17 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
// Extract payload data
|
||||
const { updateReorderedItem } = action.payload;
|
||||
if(type == "request"){
|
||||
if (type == 'request') {
|
||||
const params = item.draft.request.vars.req;
|
||||
|
||||
item.draft.request.vars.req = updateReorderedItem.map((uid) => {
|
||||
return params.find((param) => param.uid === uid);
|
||||
});
|
||||
return params.find((param) => param.uid === uid);
|
||||
});
|
||||
} else if (type === 'response') {
|
||||
const params = item.draft.request.vars.res;
|
||||
|
||||
item.draft.request.vars.res = updateReorderedItem.map((uid) => {
|
||||
return params.find((param) => param.uid === uid);
|
||||
return params.find((param) => param.uid === uid);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1755,7 +1794,7 @@ export const collectionsSlice = createSlice({
|
||||
break;
|
||||
case 'ntlm':
|
||||
set(collection, 'root.request.auth.ntlm', action.payload.content);
|
||||
break;
|
||||
break;
|
||||
case 'oauth2':
|
||||
set(collection, 'root.request.auth.oauth2', action.payload.content);
|
||||
break;
|
||||
@@ -1765,6 +1804,9 @@ export const collectionsSlice = createSlice({
|
||||
case 'apikey':
|
||||
set(collection, 'root.request.auth.apikey', action.payload.content);
|
||||
break;
|
||||
case 'ws':
|
||||
set(collection, 'root.request.auth.ws', action.payload.content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1960,6 +2002,9 @@ export const collectionsSlice = createSlice({
|
||||
case 'wsse':
|
||||
set(folder, 'root.request.auth.wsse', action.payload.content);
|
||||
break;
|
||||
case 'ws':
|
||||
set(folder, 'root.request.auth.ws', action.payload.content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2108,7 +2153,7 @@ export const collectionsSlice = createSlice({
|
||||
name: directoryName,
|
||||
collapsed: true,
|
||||
type: 'folder',
|
||||
items: [],
|
||||
items: []
|
||||
};
|
||||
currentSubItems.push(childItem);
|
||||
}
|
||||
@@ -2310,7 +2355,7 @@ export const collectionsSlice = createSlice({
|
||||
const { requestUid, itemUid, collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) return;
|
||||
|
||||
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (!item) return;
|
||||
|
||||
@@ -2339,11 +2384,11 @@ export const collectionsSlice = createSlice({
|
||||
item.preRequestScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
if(type === 'post-response-script-execution') {
|
||||
if (type === 'post-response-script-execution') {
|
||||
item.postResponseScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
if(type === 'test-script-execution') {
|
||||
if (type === 'test-script-execution') {
|
||||
item.testScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
@@ -2358,7 +2403,7 @@ export const collectionsSlice = createSlice({
|
||||
if (type === 'request-sent') {
|
||||
const { cancelTokenUid, requestSent } = action.payload;
|
||||
item.requestSent = requestSent;
|
||||
|
||||
|
||||
// sometimes the response is received before the request-sent event arrives
|
||||
if (item.requestState === 'queued') {
|
||||
item.requestState = 'sending';
|
||||
@@ -2375,12 +2420,12 @@ export const collectionsSlice = createSlice({
|
||||
const { results } = action.payload;
|
||||
item.testResults = results;
|
||||
}
|
||||
|
||||
|
||||
if (type === 'test-results-pre-request') {
|
||||
const { results } = action.payload;
|
||||
item.preRequestTestResults = results;
|
||||
}
|
||||
|
||||
|
||||
if (type === 'test-results-post-response') {
|
||||
const { results } = action.payload;
|
||||
item.postResponseTestResults = results;
|
||||
@@ -2491,7 +2536,7 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
if (collection) {
|
||||
collection.runnerResult = null;
|
||||
collection.runnerTags = { include: [], exclude: [] }
|
||||
collection.runnerTags = { include: [], exclude: [] };
|
||||
collection.runnerTagsEnabled = false;
|
||||
collection.runnerConfiguration = null;
|
||||
}
|
||||
@@ -2561,14 +2606,14 @@ export const collectionsSlice = createSlice({
|
||||
);
|
||||
|
||||
// Add the new credential with folderUid and itemUid
|
||||
filteredOauth2Credentials.push({
|
||||
collectionUid,
|
||||
folderUid,
|
||||
itemUid,
|
||||
url,
|
||||
filteredOauth2Credentials.push({
|
||||
collectionUid,
|
||||
folderUid,
|
||||
itemUid,
|
||||
url,
|
||||
credentials,
|
||||
credentialsId,
|
||||
debugInfo
|
||||
debugInfo
|
||||
});
|
||||
|
||||
collection.oauth2Credentials = filteredOauth2Credentials;
|
||||
@@ -2577,9 +2622,9 @@ export const collectionsSlice = createSlice({
|
||||
collection.timeline = [];
|
||||
}
|
||||
|
||||
if(debugInfo) {
|
||||
if (debugInfo) {
|
||||
collection.timeline.push({
|
||||
type: "oauth2",
|
||||
type: 'oauth2',
|
||||
collectionUid,
|
||||
folderUid,
|
||||
itemUid,
|
||||
@@ -2591,7 +2636,7 @@ export const collectionsSlice = createSlice({
|
||||
url,
|
||||
credentials,
|
||||
credentialsId,
|
||||
debugInfo: debugInfo.data,
|
||||
debugInfo: debugInfo.data
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2606,8 +2651,7 @@ export const collectionsSlice = createSlice({
|
||||
let collectionOauth2Credentials = cloneDeep(collection.oauth2Credentials);
|
||||
const filteredOauth2Credentials = filter(
|
||||
collectionOauth2Credentials,
|
||||
(creds) =>
|
||||
!(creds.url === url && creds.collectionUid === collectionUid)
|
||||
(creds) => !(creds.url === url && creds.collectionUid === collectionUid)
|
||||
);
|
||||
collection.oauth2Credentials = filteredOauth2Credentials;
|
||||
}
|
||||
@@ -2618,8 +2662,7 @@ export const collectionsSlice = createSlice({
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
const oauth2Credential = find(
|
||||
collection?.oauth2Credentials || [],
|
||||
(creds) =>
|
||||
creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId
|
||||
(creds) => creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId
|
||||
);
|
||||
return oauth2Credential;
|
||||
},
|
||||
@@ -2627,7 +2670,7 @@ export const collectionsSlice = createSlice({
|
||||
updateFolderAuthMode: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
|
||||
|
||||
if (folder) {
|
||||
set(folder, 'root.request.auth', {});
|
||||
set(folder, 'root.request.auth.mode', action.payload.mode);
|
||||
@@ -2675,7 +2718,7 @@ export const collectionsSlice = createSlice({
|
||||
updateCollectionTagsList: (state, action) => {
|
||||
const { collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
|
||||
if (collection) {
|
||||
collection.allTags = getUniqueTagsFromItems(collection.items);
|
||||
}
|
||||
@@ -2683,6 +2726,109 @@ export const collectionsSlice = createSlice({
|
||||
updateActiveConnections: (state, action) => {
|
||||
state.activeConnections = [...action.payload.activeConnectionIds];
|
||||
},
|
||||
runWsRequestEvent: (state, action) => {
|
||||
const { itemUid, collectionUid, eventType, eventData } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) return;
|
||||
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (!item) return;
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
|
||||
if (eventType === 'request') {
|
||||
item.requestSent = eventData;
|
||||
item.requestSent.timestamp = Date.now();
|
||||
item.response = {
|
||||
initiatedWsResponse,
|
||||
statusText: 'CONNECTING'
|
||||
};
|
||||
}
|
||||
|
||||
if (!collection.timeline) {
|
||||
collection.timeline = [];
|
||||
}
|
||||
|
||||
collection.timeline.push({
|
||||
type: 'request',
|
||||
eventType: eventType,
|
||||
collectionUid: collection.uid,
|
||||
folderUid: null,
|
||||
itemUid: item.uid,
|
||||
timestamp: Date.now(),
|
||||
data: {
|
||||
request: eventData || item.requestSent || item.request,
|
||||
timestamp: Date.now(),
|
||||
eventData: eventData
|
||||
}
|
||||
});
|
||||
},
|
||||
wsResponseReceived: (state, action) => {
|
||||
const { itemUid, collectionUid, eventType, eventData } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (!collection) return;
|
||||
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
|
||||
if (!item) return;
|
||||
|
||||
// Get current response state or create initial state
|
||||
const currentResponse = item.response || initiatedWsResponse;
|
||||
const timestamp = item?.requestSent?.timestamp;
|
||||
let updatedResponse = { ...currentResponse, duration: Date.now() - (timestamp || Date.now()) };
|
||||
|
||||
// Process based on event type
|
||||
switch (eventType) {
|
||||
case 'message':
|
||||
const { message, direction } = eventData;
|
||||
|
||||
// Add message to responses list
|
||||
updatedResponse.responses = [
|
||||
...(currentResponse?.responses || []),
|
||||
{
|
||||
message,
|
||||
direction,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
];
|
||||
break;
|
||||
|
||||
case 'open':
|
||||
updatedResponse.status = 'CONNECTED';
|
||||
updatedResponse.statusText = 'CONNECTED';
|
||||
updatedResponse.statusCode = 0;
|
||||
break;
|
||||
|
||||
case 'close':
|
||||
const { code, reason } = eventData;
|
||||
updatedResponse.status = 'CLOSED';
|
||||
updatedResponse.statusCode = code;
|
||||
updatedResponse.statusText = wsStatusCodes[code] || 'CLOSED';
|
||||
updatedResponse.statusDescription = reason;
|
||||
|
||||
// Handle error status (non-normal closure)
|
||||
if (code !== 1000) {
|
||||
updatedResponse.isError = true;
|
||||
updatedResponse.error = reason || `WebSocket closed with code ${code}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
const errorDetails = eventData.error || eventData.message;
|
||||
updatedResponse.isError = true;
|
||||
updatedResponse.error = errorDetails || 'WebSocket error occurred';
|
||||
updatedResponse.status = 'ERROR';
|
||||
updatedResponse.statusText = 'ERROR';
|
||||
break;
|
||||
|
||||
case 'connecting':
|
||||
updatedResponse.status = 'CONNECTING';
|
||||
updatedResponse.statusText = 'CONNECTING';
|
||||
break;
|
||||
}
|
||||
|
||||
item.response = updatedResponse;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2810,7 +2956,9 @@ export const {
|
||||
addRequestTag,
|
||||
deleteRequestTag,
|
||||
updateCollectionTagsList,
|
||||
updateActiveConnections
|
||||
updateActiveConnections,
|
||||
runWsRequestEvent,
|
||||
wsResponseReceived
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
export default collectionsSlice.reducer;
|
||||
|
||||
@@ -21,12 +21,8 @@ export const tabsSlice = createSlice({
|
||||
reducers: {
|
||||
addTab: (state, action) => {
|
||||
const { uid, collectionUid, type, requestPaneTab, preview } = action.payload;
|
||||
const nonReplaceableTabTypes = [
|
||||
"variables",
|
||||
"collection-runner",
|
||||
"security-settings",
|
||||
];
|
||||
|
||||
const nonReplaceableTabTypes = ['variables', 'collection-runner', 'security-settings'];
|
||||
|
||||
const existingTab = find(state.tabs, (tab) => tab.uid === uid);
|
||||
if (existingTab) {
|
||||
state.activeTabUid = existingTab.uid;
|
||||
@@ -43,12 +39,17 @@ export const tabsSlice = createSlice({
|
||||
|
||||
// Determine the default requestPaneTab based on request type
|
||||
let defaultRequestPaneTab = 'params';
|
||||
if (type === 'grpc-request') {
|
||||
if (type === 'grpc-request' || type === 'ws-request') {
|
||||
defaultRequestPaneTab = 'body';
|
||||
} else if (type === 'graphql-request') {
|
||||
defaultRequestPaneTab = 'query';
|
||||
}
|
||||
|
||||
let defaultResponsePaneTab = 'response';
|
||||
if (type === 'ws-request') {
|
||||
defaultResponsePaneTab = 'messages';
|
||||
}
|
||||
|
||||
const lastTab = state.tabs[state.tabs.length - 1];
|
||||
if (state.tabs.length > 0 && lastTab.preview) {
|
||||
state.tabs[state.tabs.length - 1] = {
|
||||
@@ -56,18 +57,16 @@ export const tabsSlice = createSlice({
|
||||
collectionUid,
|
||||
requestPaneWidth: null,
|
||||
requestPaneTab: requestPaneTab || defaultRequestPaneTab,
|
||||
responsePaneTab: 'response',
|
||||
responsePaneTab: defaultResponsePaneTab,
|
||||
type: type || 'request',
|
||||
preview: preview !== undefined
|
||||
? preview
|
||||
: !nonReplaceableTabTypes.includes(type),
|
||||
preview: preview !== undefined ? preview : !nonReplaceableTabTypes.includes(type),
|
||||
...(uid ? { folderUid: uid } : {})
|
||||
};
|
||||
|
||||
state.activeTabUid = uid;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
state.tabs.push({
|
||||
uid,
|
||||
collectionUid,
|
||||
@@ -77,9 +76,7 @@ export const tabsSlice = createSlice({
|
||||
responsePaneScrollPosition: null,
|
||||
type: type || 'request',
|
||||
...(uid ? { folderUid: uid } : {}),
|
||||
preview: preview !== undefined
|
||||
? preview
|
||||
: !nonReplaceableTabTypes.includes(type)
|
||||
preview: preview !== undefined ? preview : !nonReplaceableTabTypes.includes(type)
|
||||
});
|
||||
state.activeTabUid = uid;
|
||||
},
|
||||
@@ -195,4 +192,4 @@ export const {
|
||||
makeTabPermanent
|
||||
} = tabsSlice.actions;
|
||||
|
||||
export default tabsSlice.reducer;
|
||||
export default tabsSlice.reducer;
|
||||
|
||||
@@ -94,15 +94,41 @@ const darkTheme = {
|
||||
// customize these colors if needed
|
||||
patch: '#d69956',
|
||||
options: '#d69956',
|
||||
head: '#d69956',
|
||||
head: '#d69956'
|
||||
},
|
||||
grpc: '#6366f1'
|
||||
grpc: '#6366f1',
|
||||
ws: '#f59e0b'
|
||||
},
|
||||
|
||||
requestTabPanel: {
|
||||
url: {
|
||||
bg: '#3D3D3D',
|
||||
icon: 'rgb(204, 204, 204)'
|
||||
border: '#444',
|
||||
icon: 'rgb(204, 204, 204)',
|
||||
hoverBg: '#4a4a4a',
|
||||
errorHoverBg: '#4a2a2a'
|
||||
},
|
||||
body: {
|
||||
bg: '#3D3D3D',
|
||||
border: '#444',
|
||||
headerBg: '#2a2a2a',
|
||||
footerBg: '#2a2a2a',
|
||||
addBtnBg: '#6366f1',
|
||||
addBtnColor: '#ffffff',
|
||||
addBtnBorder: '#6366f1',
|
||||
addBtnHoverBg: '#4f46e5',
|
||||
addBtnHoverBorder: '#4f46e5',
|
||||
messageBorder: '#555',
|
||||
messageHeaderBg: '#2a2a2a',
|
||||
inputColor: '#ffffff',
|
||||
placeholderColor: '#888',
|
||||
textareaBg: '#2a2a2a',
|
||||
textareaColor: '#ffffff',
|
||||
removeBtnColor: '#888',
|
||||
removeBtnHoverBg: '#4a2a2a',
|
||||
removeBtnHoverColor: '#f06f57',
|
||||
statusColor: '#888',
|
||||
shortcutBg: '#555'
|
||||
},
|
||||
dragbar: {
|
||||
border: '#444',
|
||||
@@ -252,7 +278,7 @@ const darkTheme = {
|
||||
border: '#373737',
|
||||
placeholder: {
|
||||
color: '#a2a2a2',
|
||||
opacity: 0.50
|
||||
opacity: 0.5
|
||||
},
|
||||
gutter: {
|
||||
bg: '#262626'
|
||||
@@ -293,6 +319,12 @@ const darkTheme = {
|
||||
hoverBg: 'rgba(102, 102, 102, 0.08)',
|
||||
transition: 'all 0.1s ease'
|
||||
},
|
||||
tooltip: {
|
||||
bg: '#1f1f1f',
|
||||
color: '#ffffff',
|
||||
shortcutColor: '#f59e0b'
|
||||
},
|
||||
|
||||
infoTip: {
|
||||
bg: '#1f1f1f',
|
||||
border: '#333333',
|
||||
@@ -303,6 +335,23 @@ const darkTheme = {
|
||||
border: '#323233',
|
||||
color: 'rgb(169, 169, 169)'
|
||||
},
|
||||
responseTabPanel: {
|
||||
bg: '#3D3D3D',
|
||||
border: '#444',
|
||||
headerBg: '#2a2a2a',
|
||||
sectionBorder: '#555',
|
||||
labelColor: '#888',
|
||||
messageBorder: '#555',
|
||||
messageHeaderBg: '#2a2a2a',
|
||||
messageTextColor: '#ffffff',
|
||||
copyBtnColor: '#888',
|
||||
copyBtnHoverBg: '#4a2a2a',
|
||||
copyBtnHoverColor: '#f06f57',
|
||||
errorBg: '#2a1a1a',
|
||||
errorBorder: '#4a2a2a',
|
||||
errorColor: '#f06f57'
|
||||
},
|
||||
|
||||
console: {
|
||||
bg: '#1e1e1e',
|
||||
headerBg: '#2d2d30',
|
||||
|
||||
@@ -96,13 +96,39 @@ const lightTheme = {
|
||||
options: '#ca7811',
|
||||
head: '#ca7811'
|
||||
},
|
||||
grpc: '#6366f1'
|
||||
grpc: '#6366f1',
|
||||
ws: '#f59e0b'
|
||||
},
|
||||
|
||||
requestTabPanel: {
|
||||
url: {
|
||||
bg: '#f3f3f3',
|
||||
icon: '#515151'
|
||||
border: '#efefef',
|
||||
icon: '#515151',
|
||||
hoverBg: '#f9fafb',
|
||||
errorHoverBg: '#fef2f2'
|
||||
},
|
||||
body: {
|
||||
bg: '#ffffff',
|
||||
border: '#e5e7eb',
|
||||
headerBg: '#f9fafb',
|
||||
footerBg: '#f9fafb',
|
||||
addBtnBg: '#6366f1',
|
||||
addBtnColor: '#ffffff',
|
||||
addBtnBorder: '#6366f1',
|
||||
addBtnHoverBg: '#4f46e5',
|
||||
addBtnHoverBorder: '#4f46e5',
|
||||
messageBorder: '#d1d5db',
|
||||
messageHeaderBg: '#f9fafb',
|
||||
inputColor: '#374151',
|
||||
placeholderColor: '#9ca3af',
|
||||
textareaBg: '#f9fafb',
|
||||
textareaColor: '#374151',
|
||||
removeBtnColor: '#6b7280',
|
||||
removeBtnHoverBg: '#fef2f2',
|
||||
removeBtnHoverColor: '#dc2626',
|
||||
statusColor: '#6b7280',
|
||||
shortcutBg: '#e5e7eb'
|
||||
},
|
||||
dragbar: {
|
||||
border: '#efefef',
|
||||
@@ -294,12 +320,36 @@ const lightTheme = {
|
||||
hoverBg: 'rgba(139, 139, 139, 0.05)', // Matching the border color with reduced opacity
|
||||
transition: 'all 0.1s ease'
|
||||
},
|
||||
|
||||
tooltip: {
|
||||
bg: '#374151',
|
||||
color: '#ffffff',
|
||||
shortcutColor: '#f59e0b'
|
||||
},
|
||||
|
||||
infoTip: {
|
||||
bg: 'white',
|
||||
border: '#e0e0e0',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'
|
||||
},
|
||||
|
||||
responseTabPanel: {
|
||||
bg: '#ffffff',
|
||||
border: '#e5e7eb',
|
||||
headerBg: '#f9fafb',
|
||||
sectionBorder: '#e5e7eb',
|
||||
labelColor: '#6b7280',
|
||||
messageBorder: '#d1d5db',
|
||||
messageHeaderBg: '#f9fafb',
|
||||
messageTextColor: '#374151',
|
||||
copyBtnColor: '#6b7280',
|
||||
copyBtnHoverBg: '#f3f4f6',
|
||||
copyBtnHoverColor: '#dc2626',
|
||||
errorBg: '#fef2f2',
|
||||
errorBorder: '#fecaca',
|
||||
errorColor: '#dc2626'
|
||||
},
|
||||
|
||||
statusBar: {
|
||||
border: '#E9E9E9',
|
||||
color: 'rgb(100, 100, 100)'
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useSelector } from 'react-redux';
|
||||
* Contains all available beta feature keys
|
||||
*/
|
||||
export const BETA_FEATURES = Object.freeze({
|
||||
GRPC: 'grpc'
|
||||
GRPC: 'grpc',
|
||||
WEBSOCKET: 'websocket'
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,7 +29,7 @@ export const deleteUidsInItems = (items) => {
|
||||
*/
|
||||
export const transformItem = (items = []) => {
|
||||
each(items, (item) => {
|
||||
if (['http-request', 'graphql-request', 'grpc-request'].includes(item.type)) {
|
||||
if (['http-request', 'graphql-request', 'grpc-request', 'ws-request'].includes(item.type)) {
|
||||
if (item.type === 'graphql-request') {
|
||||
item.type = 'graphql';
|
||||
}
|
||||
@@ -41,6 +41,10 @@ export const transformItem = (items = []) => {
|
||||
if (item.type === 'grpc-request') {
|
||||
item.type = 'grpc';
|
||||
}
|
||||
|
||||
if (item.type === 'ws-request') {
|
||||
item.type = 'ws';
|
||||
}
|
||||
}
|
||||
|
||||
if (item.items && item.items.length) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash';
|
||||
import { cloneDeep, each, filter, find, findIndex, get, isEqual, isString, map, sortBy } from 'lodash';
|
||||
import { uuid } from 'utils/common';
|
||||
import { sortByNameThenSequence } from 'utils/common/index';
|
||||
import path from 'utils/common/path';
|
||||
@@ -140,7 +140,7 @@ export const areItemsLoading = (folder) => {
|
||||
if (!folder || folder.isLoading) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
let flattenedItems = flattenItems(folder.items);
|
||||
return flattenedItems?.reduce((isLoading, i) => {
|
||||
if (i?.loading) {
|
||||
@@ -148,13 +148,13 @@ export const areItemsLoading = (folder) => {
|
||||
}
|
||||
return isLoading;
|
||||
}, false);
|
||||
}
|
||||
};
|
||||
|
||||
export const getItemsLoadStats = (folder) => {
|
||||
let loadingCount = 0;
|
||||
let flattenedItems = flattenItems(folder.items);
|
||||
flattenedItems?.forEach(i => {
|
||||
if(i?.loading) {
|
||||
flattenedItems?.forEach((i) => {
|
||||
if (i?.loading) {
|
||||
loadingCount += 1;
|
||||
}
|
||||
});
|
||||
@@ -162,7 +162,7 @@ export const getItemsLoadStats = (folder) => {
|
||||
loading: loadingCount,
|
||||
total: flattenedItems?.length
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const transformCollectionToSaveToExportAsFile = (collection, options = {}) => {
|
||||
const copyHeaders = (headers) => {
|
||||
@@ -222,9 +222,9 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
filePath: param.filePath,
|
||||
contentType: param.contentType,
|
||||
selected: param.selected
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const copyItems = (sourceItems, destItems) => {
|
||||
each(sourceItems, (si) => {
|
||||
@@ -232,7 +232,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
return;
|
||||
}
|
||||
|
||||
const isGrpcRequest = si.type === 'grpc-request'
|
||||
const isGrpcRequest = si.type === 'grpc-request';
|
||||
|
||||
const di = {
|
||||
uid: si.uid,
|
||||
@@ -274,7 +274,6 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
di.request.protoPath = si.request.protoPath;
|
||||
delete di.request.params;
|
||||
}
|
||||
|
||||
|
||||
// Handle auth object dynamically
|
||||
di.request.auth = {
|
||||
@@ -315,7 +314,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
password: get(si.request, 'auth.ntlm.password', ''),
|
||||
domain: get(si.request, 'auth.ntlm.domain', '')
|
||||
};
|
||||
break;
|
||||
break;
|
||||
case 'oauth2':
|
||||
let grantType = get(si.request, 'auth.oauth2.grantType', '');
|
||||
switch (grantType) {
|
||||
@@ -335,7 +334,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', ''),
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true)
|
||||
};
|
||||
break;
|
||||
case 'authorization_code':
|
||||
@@ -355,7 +354,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', ''),
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true)
|
||||
};
|
||||
break;
|
||||
case 'implicit':
|
||||
@@ -370,7 +369,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
|
||||
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'),
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true)
|
||||
};
|
||||
break;
|
||||
case 'client_credentials':
|
||||
@@ -387,7 +386,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', ''),
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true)
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -414,10 +413,10 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
}
|
||||
|
||||
if (di.request.body.mode === 'grpc') {
|
||||
di.request.body.grpc = di.request.body.grpc.map(({name, content}, index) => ({
|
||||
di.request.body.grpc = di.request.body.grpc.map(({ name, content }, index) => ({
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
content: replaceTabsWithSpaces(content)
|
||||
}))
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,11 +596,17 @@ export const transformRequestToSaveToFilesystem = (item) => {
|
||||
if (_item.type === 'grpc-request') {
|
||||
itemToSave.request.methodType = _item.request.methodType;
|
||||
itemToSave.request.protoPath = _item.request.protoPath;
|
||||
delete itemToSave.request.params
|
||||
delete itemToSave.request.params;
|
||||
}
|
||||
|
||||
if (_item.type === 'ws-request') {
|
||||
delete itemToSave.request.method;
|
||||
delete itemToSave.request.methodType;
|
||||
delete itemToSave.request.params;
|
||||
}
|
||||
|
||||
// Only process params for non-gRPC requests
|
||||
if (_item.type !== 'grpc-request') {
|
||||
if (!['grpc-request', 'ws-request'].includes(_item.type)) {
|
||||
each(_item.request.params, (param) => {
|
||||
itemToSave.request.params.push({
|
||||
uid: param.uid,
|
||||
@@ -634,7 +639,17 @@ export const transformRequestToSaveToFilesystem = (item) => {
|
||||
if (itemToSave.request.body.mode === 'grpc') {
|
||||
itemToSave.request.body = {
|
||||
...itemToSave.request.body,
|
||||
grpc: itemToSave.request.body.grpc.map(({name, content}, index) => ({
|
||||
grpc: itemToSave.request.body.grpc.map(({ name, content }, index) => ({
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
content: replaceTabsWithSpaces(content)
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
if (itemToSave.request.body.mode === 'ws') {
|
||||
itemToSave.request.body = {
|
||||
...itemToSave.request.body,
|
||||
grpc: itemToSave.request.body.ws.map(({ name, content }, index) => ({
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
content: replaceTabsWithSpaces(content)
|
||||
}))
|
||||
@@ -668,7 +683,11 @@ export const deleteItemInCollectionByPathname = (pathname, collection) => {
|
||||
};
|
||||
|
||||
export const isItemARequest = (item) => {
|
||||
return item.hasOwnProperty('request') && ['http-request', 'graphql-request', 'grpc-request'].includes(item.type) && !item.items;
|
||||
return (
|
||||
item.hasOwnProperty('request') &&
|
||||
['http-request', 'graphql-request', 'grpc-request', 'ws-request'].includes(item.type) &&
|
||||
!item.items
|
||||
);
|
||||
};
|
||||
|
||||
export const isItemAFolder = (item) => {
|
||||
@@ -737,7 +756,7 @@ export const humanizeRequestAuthMode = (mode) => {
|
||||
case 'ntlm': {
|
||||
label = 'NTLM';
|
||||
break;
|
||||
}
|
||||
}
|
||||
case 'oauth2': {
|
||||
label = 'OAuth 2.0';
|
||||
break;
|
||||
@@ -851,14 +870,14 @@ export const getDefaultRequestPaneTab = (item) => {
|
||||
return 'query';
|
||||
}
|
||||
|
||||
if (item.type === 'grpc-request') {
|
||||
if (['ws-request', 'grpc-request'].includes(item.type)) {
|
||||
return 'body';
|
||||
}
|
||||
};
|
||||
|
||||
export const getGlobalEnvironmentVariables = ({ globalEnvironments, activeGlobalEnvironmentUid }) => {
|
||||
let variables = {};
|
||||
const environment = globalEnvironments?.find(env => env?.uid === activeGlobalEnvironmentUid);
|
||||
const environment = globalEnvironments?.find((env) => env?.uid === activeGlobalEnvironmentUid);
|
||||
if (environment) {
|
||||
each(environment.variables, (variable) => {
|
||||
if (variable.name && variable.enabled) {
|
||||
@@ -870,7 +889,7 @@ export const getGlobalEnvironmentVariables = ({ globalEnvironments, activeGlobal
|
||||
};
|
||||
|
||||
export const getGlobalEnvironmentVariablesMasked = ({ globalEnvironments, activeGlobalEnvironmentUid }) => {
|
||||
const environment = globalEnvironments?.find(env => env?.uid === activeGlobalEnvironmentUid);
|
||||
const environment = globalEnvironments?.find((env) => env?.uid === activeGlobalEnvironmentUid);
|
||||
|
||||
if (environment && Array.isArray(environment.variables)) {
|
||||
return environment.variables
|
||||
@@ -881,7 +900,6 @@ export const getGlobalEnvironmentVariablesMasked = ({ globalEnvironments, active
|
||||
return [];
|
||||
};
|
||||
|
||||
|
||||
export const getEnvironmentVariables = (collection) => {
|
||||
let variables = {};
|
||||
if (collection) {
|
||||
@@ -942,7 +960,7 @@ export const getTotalRequestCountInCollection = (collection) => {
|
||||
};
|
||||
|
||||
export const getAllVariables = (collection, item) => {
|
||||
if(!collection) return {};
|
||||
if (!collection) return {};
|
||||
const envVariables = getEnvironmentVariables(collection);
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
let { collectionVariables, folderVariables, requestVariables } = mergeVars(collection, requestTreePath);
|
||||
@@ -961,8 +979,8 @@ export const getAllVariables = (collection, item) => {
|
||||
...envVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...runtimeVariables,
|
||||
}
|
||||
...runtimeVariables
|
||||
};
|
||||
|
||||
const maskedEnvVariables = getEnvironmentVariablesMasked(collection) || [];
|
||||
const maskedGlobalEnvVariables = collection?.globalEnvSecrets || [];
|
||||
@@ -972,7 +990,9 @@ export const getAllVariables = (collection, item) => {
|
||||
|
||||
const uniqueMaskedVariables = [...new Set([...filteredMaskedEnvVariables, ...filteredMaskedGlobalEnvVariables])];
|
||||
|
||||
const oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials })
|
||||
const oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({
|
||||
oauth2Credentials: collection?.oauth2Credentials
|
||||
});
|
||||
|
||||
return {
|
||||
...globalEnvironmentVariables,
|
||||
@@ -1082,7 +1102,6 @@ export const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = []
|
||||
return credentialsVariables;
|
||||
};
|
||||
|
||||
|
||||
// item sequence utils - START
|
||||
|
||||
export const resetSequencesInFolder = (folderItems) => {
|
||||
@@ -1114,7 +1133,7 @@ export const getReorderedItemsInTargetDirectory = ({ items, targetItemUid, dragg
|
||||
const draggedItem = findItem(itemsWithFixedSequences, draggedItemUid);
|
||||
const targetSequence = targetItem?.seq;
|
||||
const draggedSequence = draggedItem?.seq;
|
||||
itemsWithFixedSequences?.forEach(item => {
|
||||
itemsWithFixedSequences?.forEach((item) => {
|
||||
const isDraggedItem = item?.uid === draggedItemUid;
|
||||
const isBetween = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence);
|
||||
if (isBetween) {
|
||||
@@ -1126,15 +1145,15 @@ export const getReorderedItemsInTargetDirectory = ({ items, targetItemUid, dragg
|
||||
}
|
||||
});
|
||||
// only return items that have been reordered
|
||||
return itemsWithFixedSequences.filter(item =>
|
||||
items?.find(originalItem => originalItem?.uid === item?.uid)?.seq !== item?.seq
|
||||
return itemsWithFixedSequences.filter(
|
||||
(item) => items?.find((originalItem) => originalItem?.uid === item?.uid)?.seq !== item?.seq
|
||||
);
|
||||
};
|
||||
|
||||
export const getReorderedItemsInSourceDirectory = ({ items }) => {
|
||||
const itemsWithFixedSequences = resetSequencesInFolder(cloneDeep(items));
|
||||
return itemsWithFixedSequences.filter(item =>
|
||||
items?.find(originalItem => originalItem?.uid === item?.uid)?.seq !== item?.seq
|
||||
return itemsWithFixedSequences.filter(
|
||||
(item) => items?.find((originalItem) => originalItem?.uid === item?.uid)?.seq !== item?.seq
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1146,9 +1165,9 @@ export const calculateDraggedItemNewPathname = ({ draggedItem, targetItem, dropT
|
||||
const isTargetItemAFolder = isItemAFolder(targetItem);
|
||||
|
||||
if (dropType === 'inside' && (isTargetItemAFolder || isTargetTheCollection)) {
|
||||
return path.join(targetItemPathname, draggedItemFilename)
|
||||
return path.join(targetItemPathname, draggedItemFilename);
|
||||
} else if (dropType === 'adjacent') {
|
||||
return path.join(targetItemDirname, draggedItemFilename)
|
||||
return path.join(targetItemDirname, draggedItemFilename);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -1158,10 +1177,10 @@ export const calculateDraggedItemNewPathname = ({ draggedItem, targetItem, dropT
|
||||
export const getUniqueTagsFromItems = (items = []) => {
|
||||
const allTags = new Set();
|
||||
const getTags = (items) => {
|
||||
items.forEach(item => {
|
||||
items.forEach((item) => {
|
||||
if (isItemARequest(item)) {
|
||||
const tags = item.draft ? get(item, 'draft.tags', []) : get(item, 'tags', []);
|
||||
tags.forEach(tag => allTags.add(tag));
|
||||
tags.forEach((tag) => allTags.add(tag));
|
||||
}
|
||||
if (item.items) {
|
||||
getTags(item.items);
|
||||
@@ -1172,10 +1191,9 @@ export const getUniqueTagsFromItems = (items = []) => {
|
||||
return Array.from(allTags).sort();
|
||||
};
|
||||
|
||||
|
||||
export const getRequestItemsForCollectionRun = ({ recursive, items = [], tags }) => {
|
||||
let requestItems = [];
|
||||
|
||||
|
||||
if (recursive) {
|
||||
requestItems = flattenItems(items);
|
||||
} else {
|
||||
@@ -1187,7 +1205,7 @@ export const getRequestItemsForCollectionRun = ({ recursive, items = [], tags })
|
||||
}
|
||||
|
||||
const requestTypes = ['http-request', 'graphql-request'];
|
||||
requestItems = requestItems.filter(request => requestTypes.includes(request.type));
|
||||
requestItems = requestItems.filter((request) => requestTypes.includes(request.type));
|
||||
|
||||
if (tags && tags.include && tags.exclude) {
|
||||
const includeTags = tags.include ? tags.include : [];
|
||||
@@ -1203,4 +1221,4 @@ export const getRequestItemsForCollectionRun = ({ recursive, items = [], tags })
|
||||
|
||||
export const getPropertyFromDraftOrRequest = (item, propertyKey, defaultValue = null) => {
|
||||
return item.draft ? get(item, `draft.${propertyKey}`, defaultValue) : get(item, propertyKey, defaultValue);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
|
||||
.then((response) => {
|
||||
// if there is an error, we return the response object as is
|
||||
if (response?.error) {
|
||||
resolve(response)
|
||||
resolve(response);
|
||||
}
|
||||
resolve({
|
||||
state: 'success',
|
||||
@@ -28,19 +28,17 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
|
||||
export const sendGrpcRequest = async (item, collection, environment, runtimeVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
startGrpcRequest(item, collection, environment, runtimeVariables)
|
||||
.then((initialState) => {
|
||||
// Return an initial state object to update the UI
|
||||
// The real response data will be handled by event listeners
|
||||
resolve({
|
||||
...initialState,
|
||||
timeline: []
|
||||
});
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
.then((initialState) => {
|
||||
// Return an initial state object to update the UI
|
||||
// The real response data will be handled by event listeners
|
||||
resolve({
|
||||
...initialState,
|
||||
timeline: []
|
||||
});
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const sendHttpRequest = async (item, collection, environment, runtimeVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -80,19 +78,20 @@ export const startGrpcRequest = async (item, collection, environment, runtimeVar
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
const request = item.draft ? item.draft : item;
|
||||
|
||||
ipcRenderer.invoke('grpc:start-connection', {
|
||||
request,
|
||||
collection,
|
||||
environment,
|
||||
runtimeVariables
|
||||
})
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
ipcRenderer
|
||||
.invoke('grpc:start-connection', {
|
||||
request,
|
||||
collection,
|
||||
environment,
|
||||
runtimeVariables
|
||||
})
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -105,9 +104,7 @@ export const startGrpcRequest = async (item, collection, environment, runtimeVar
|
||||
export const sendGrpcMessage = async (item, collectionUid, message) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('grpc:send-message', item.uid, collectionUid, message)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
ipcRenderer.invoke('grpc:send-message', item.uid, collectionUid, message).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -119,9 +116,7 @@ export const sendGrpcMessage = async (item, collectionUid, message) => {
|
||||
export const cancelGrpcRequest = async (requestId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('grpc:cancel', requestId)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
ipcRenderer.invoke('grpc:cancel', requestId).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -133,9 +128,7 @@ export const cancelGrpcRequest = async (requestId) => {
|
||||
export const endGrpcStream = async (requestId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('grpc:end', requestId)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
ipcRenderer.invoke('grpc:end', requestId).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -175,8 +168,9 @@ export const endGrpcConnection = async (connectionId) => {
|
||||
export const isGrpcConnectionActive = async (connectionId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('grpc:is-connection-active', connectionId)
|
||||
.then(response => {
|
||||
ipcRenderer
|
||||
.invoke('grpc:is-connection-active', connectionId)
|
||||
.then((response) => {
|
||||
if (response.success) {
|
||||
resolve(response.isActive);
|
||||
} else {
|
||||
@@ -185,7 +179,7 @@ export const isGrpcConnectionActive = async (connectionId) => {
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.error('Failed to check connection status:', err);
|
||||
// On error, assume the connection is not active
|
||||
resolve(false);
|
||||
@@ -203,13 +197,111 @@ export const isGrpcConnectionActive = async (connectionId) => {
|
||||
export const generateGrpcSampleMessage = async (methodPath, existingMessage = null, options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('grpc:generate-sample-message', {
|
||||
methodPath,
|
||||
existingMessage,
|
||||
options
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
|
||||
ipcRenderer
|
||||
.invoke('grpc:generate-sample-message', {
|
||||
methodPath,
|
||||
existingMessage,
|
||||
options
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const connectWS = async (item, collection, environment, runtimeVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
startWsConnection(item, collection, environment, runtimeVariables)
|
||||
.then((initialState) => {
|
||||
// Return an initial state object to update the UI
|
||||
// The real response data will be handled by event listeners
|
||||
resolve({
|
||||
...initialState,
|
||||
timeline: []
|
||||
});
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
});
|
||||
};
|
||||
|
||||
export const sendWsRequest = (item, collection, environment, runtimeVariables) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const ensureConnection = async () => {
|
||||
const connectionStatus = await isWsConnectionActive(item.uid);
|
||||
if (!connectionStatus.isActive) {
|
||||
await connectWS(item, collection, environment, runtimeVariables);
|
||||
}
|
||||
};
|
||||
ensureConnection().then(() => {
|
||||
const { request } = item.draft ? item.draft : item;
|
||||
sendWsMessage(item, collection.uid, request.body.ws[0].content)
|
||||
.then((initialState) => {
|
||||
// Return an initial state object to update the UI
|
||||
// The real response data will be handled by event listeners
|
||||
resolve({
|
||||
...initialState,
|
||||
timeline: []
|
||||
});
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const startWsConnection = async (item, collection, environment, runtimeVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
const request = item.draft ? item.draft : item;
|
||||
|
||||
ipcRenderer
|
||||
.invoke('ws:start-connection', {
|
||||
request,
|
||||
collection,
|
||||
environment,
|
||||
runtimeVariables
|
||||
})
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a message to an existing WebSocket connection
|
||||
* @param {string} requestId - The request ID to send a message to
|
||||
* @param {Object} message - The message to send
|
||||
* @returns {Promise<Object>} - The result of the send operation
|
||||
*/
|
||||
export const sendWsMessage = async (item, collectionUid, message) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('ws:send-message', item.uid, collectionUid, message).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes a WebSocket connection
|
||||
* @param {string} requestId - The request ID to close
|
||||
* @returns {Promise<Object>} - The result of the close operation
|
||||
*/
|
||||
export const closeWsConnection = async (requestId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('ws:close-connection', requestId).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a WebSocket connection is active
|
||||
* @param {string} requestId - The request ID to check
|
||||
* @returns {Promise<boolean>} - Whether the connection is active
|
||||
*/
|
||||
export const isWsConnectionActive = async (requestId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('ws:is-connection-active', requestId).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
107
packages/bruno-app/src/utils/network/ws-event-listeners.js
Normal file
107
packages/bruno-app/src/utils/network/ws-event-listeners.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useEffect } from 'react';
|
||||
import { wsResponseReceived, runWsRequestEvent } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
import { updateActiveConnectionsInStore } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
const useWsEventListeners = () => {
|
||||
const { ipcRenderer } = window;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isElectron()) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
ipcRenderer.invoke('renderer:ready');
|
||||
|
||||
// Handle WebSocket requestSent event
|
||||
const removeWsRequestSentListener = ipcRenderer.on('ws:request', (requestId, collectionUid, eventData) => {
|
||||
dispatch(
|
||||
runWsRequestEvent({
|
||||
eventType: 'request',
|
||||
itemUid: requestId,
|
||||
collectionUid: collectionUid,
|
||||
requestUid: requestId,
|
||||
eventData
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Handle WebSocket message event
|
||||
const removeWsMessageListener = ipcRenderer.on('ws:message', (requestId, collectionUid, eventData) => {
|
||||
dispatch(
|
||||
wsResponseReceived({
|
||||
itemUid: requestId,
|
||||
collectionUid: collectionUid,
|
||||
eventType: 'message',
|
||||
eventData: eventData
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Handle WebSocket open event
|
||||
const removeWsOpenListener = ipcRenderer.on('ws:open', (requestId, collectionUid, eventData) => {
|
||||
dispatch(
|
||||
wsResponseReceived({
|
||||
itemUid: requestId,
|
||||
collectionUid: collectionUid,
|
||||
eventType: 'open',
|
||||
eventData: eventData
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Handle WebSocket close event
|
||||
const removeWsCloseListener = ipcRenderer.on('ws:close', (requestId, collectionUid, eventData) => {
|
||||
dispatch(
|
||||
wsResponseReceived({
|
||||
itemUid: requestId,
|
||||
collectionUid: collectionUid,
|
||||
eventType: 'close',
|
||||
eventData: eventData
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Handle WebSocket error event
|
||||
const removeWsErrorListener = ipcRenderer.on('ws:error', (requestId, collectionUid, eventData) => {
|
||||
dispatch(
|
||||
wsResponseReceived({
|
||||
itemUid: requestId,
|
||||
collectionUid: collectionUid,
|
||||
eventType: 'error',
|
||||
eventData: eventData
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Handle WebSocket connecting event
|
||||
const removeWsConnectingListener = ipcRenderer.on('ws:connecting', (requestId, collectionUid, eventData) => {
|
||||
dispatch(
|
||||
wsResponseReceived({
|
||||
itemUid: requestId,
|
||||
collectionUid: collectionUid,
|
||||
eventType: 'connecting',
|
||||
eventData: eventData
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const removeWsConnectionsChangedListener = ipcRenderer.on('ws:connections-changed', (data) => {
|
||||
dispatch(updateActiveConnectionsInStore(data));
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeWsRequestSentListener();
|
||||
removeWsMessageListener();
|
||||
removeWsOpenListener();
|
||||
removeWsCloseListener();
|
||||
removeWsErrorListener();
|
||||
removeWsConnectingListener();
|
||||
removeWsConnectionsChangedListener();
|
||||
};
|
||||
}, [isElectron]);
|
||||
};
|
||||
|
||||
export default useWsEventListeners;
|
||||
@@ -1,7 +1,10 @@
|
||||
import find from 'lodash/find';
|
||||
|
||||
export const isItemARequest = (item) => {
|
||||
return item.hasOwnProperty('request') && ['http-request', 'graphql-request', 'grpc-request'].includes(item.type);
|
||||
return (
|
||||
item.hasOwnProperty('request') &&
|
||||
['http-request', 'graphql-request', 'grpc-request', 'ws-request'].includes(item.type)
|
||||
);
|
||||
};
|
||||
|
||||
export const isItemAFolder = (item) => {
|
||||
@@ -17,4 +20,4 @@ export const scrollToTheActiveTab = () => {
|
||||
if (activeTab) {
|
||||
activeTab.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,6 +27,9 @@ const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
const { NtlmClient } = require('axios-ntlm');
|
||||
const { addDigestInterceptor } = require('@usebruno/requests');
|
||||
const { encodeUrl } = require('@usebruno/common').utils;
|
||||
const { sendNetworkRequest } = require('@usebruno/requests');
|
||||
const { sendGrpcRequest } = require('@usebruno/requests');
|
||||
const { sendWsRequest } = require('@usebruno/requests');
|
||||
|
||||
const onConsoleLog = (type, args) => {
|
||||
console[type](...args);
|
||||
@@ -82,7 +85,7 @@ const runSingleRequest = async function (
|
||||
|
||||
// run pre request script
|
||||
const requestScriptFile = get(request, 'script.req');
|
||||
const collectionName = collection?.brunoConfig?.name
|
||||
const collectionName = collection?.brunoConfig?.name;
|
||||
if (requestScriptFile?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const result = await scriptRuntime.runRequestScript(
|
||||
@@ -214,7 +217,7 @@ const runSingleRequest = async function (
|
||||
|
||||
const collectionProxyConfig = get(brunoConfig, 'proxy', {});
|
||||
const collectionProxyEnabled = get(collectionProxyConfig, 'enabled', false);
|
||||
|
||||
|
||||
if (noproxy) {
|
||||
// If noproxy flag is set, don't use any proxy
|
||||
proxyMode = 'off';
|
||||
@@ -243,8 +246,12 @@ const runSingleRequest = async function (
|
||||
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
|
||||
let proxyUri;
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions));
|
||||
const proxyAuthPassword = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions));
|
||||
const proxyAuthUsername = encodeURIComponent(
|
||||
interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions)
|
||||
);
|
||||
const proxyAuthPassword = encodeURIComponent(
|
||||
interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions)
|
||||
);
|
||||
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
|
||||
} else {
|
||||
@@ -310,37 +317,34 @@ const runSingleRequest = async function (
|
||||
if (!options.disableCookies) {
|
||||
const cookieString = getCookieStringForUrl(request.url);
|
||||
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
|
||||
const existingCookieHeaderName = Object.keys(request.headers).find(
|
||||
name => name.toLowerCase() === 'cookie'
|
||||
);
|
||||
const existingCookieHeaderName = Object.keys(request.headers).find((name) => name.toLowerCase() === 'cookie');
|
||||
const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : '';
|
||||
|
||||
|
||||
// Helper function to parse cookies into an object
|
||||
const parseCookies = (str) => str.split(';').reduce((cookies, cookie) => {
|
||||
const parseCookies = (str) =>
|
||||
str.split(';').reduce((cookies, cookie) => {
|
||||
const [name, ...rest] = cookie.split('=');
|
||||
if (name && name.trim()) {
|
||||
cookies[name.trim()] = rest.join('=').trim();
|
||||
cookies[name.trim()] = rest.join('=').trim();
|
||||
}
|
||||
return cookies;
|
||||
}, {});
|
||||
|
||||
}, {});
|
||||
|
||||
const mergedCookies = {
|
||||
...parseCookies(existingCookieString),
|
||||
...parseCookies(cookieString),
|
||||
...parseCookies(existingCookieString),
|
||||
...parseCookies(cookieString)
|
||||
};
|
||||
|
||||
|
||||
const combinedCookieString = Object.entries(mergedCookies)
|
||||
.map(([name, value]) => `${name}=${value}`)
|
||||
.join('; ');
|
||||
|
||||
.map(([name, value]) => `${name}=${value}`)
|
||||
.join('; ');
|
||||
|
||||
request.headers[existingCookieHeaderName || 'Cookie'] = combinedCookieString;
|
||||
}
|
||||
}
|
||||
|
||||
// stringify the request url encoded params
|
||||
const contentTypeHeader = Object.keys(request.headers).find(
|
||||
name => name.toLowerCase() === 'content-type'
|
||||
);
|
||||
const contentTypeHeader = Object.keys(request.headers).find((name) => name.toLowerCase() === 'content-type');
|
||||
if (contentTypeHeader && request.headers[contentTypeHeader] === 'application/x-www-form-urlencoded') {
|
||||
request.data = qs.stringify(request.data, { arrayFormat: 'repeat' });
|
||||
}
|
||||
@@ -353,9 +357,9 @@ const runSingleRequest = async function (
|
||||
}
|
||||
}
|
||||
|
||||
let requestMaxRedirects = request.maxRedirects
|
||||
request.maxRedirects = 0
|
||||
|
||||
let requestMaxRedirects = request.maxRedirects;
|
||||
request.maxRedirects = 0;
|
||||
|
||||
// Set default value for requestMaxRedirects if not explicitly set
|
||||
if (requestMaxRedirects === undefined) {
|
||||
requestMaxRedirects = 5; // Default to 5 redirects
|
||||
@@ -367,7 +371,7 @@ const runSingleRequest = async function (
|
||||
const token = await getOAuth2Token(request.oauth2);
|
||||
if (token) {
|
||||
const { tokenPlacement = 'header', tokenHeaderPrefix = '', tokenQueryKey = 'access_token' } = request.oauth2;
|
||||
|
||||
|
||||
if (tokenPlacement === 'header' && token) {
|
||||
request.headers['Authorization'] = `${tokenHeaderPrefix} ${token}`.trim();
|
||||
} else if (tokenPlacement === 'url') {
|
||||
@@ -383,98 +387,112 @@ const runSingleRequest = async function (
|
||||
} catch (error) {
|
||||
console.error('OAuth2 token fetch error:', error.message);
|
||||
}
|
||||
|
||||
|
||||
// Remove oauth2 config from request to prevent it from being sent
|
||||
delete request.oauth2;
|
||||
}
|
||||
|
||||
let response, responseTime;
|
||||
try {
|
||||
|
||||
let axiosInstance = makeAxiosInstance({ requestMaxRedirects: requestMaxRedirects, disableCookies: options.disableCookies });
|
||||
if (request.ntlmConfig) {
|
||||
axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults)
|
||||
delete request.ntlmConfig;
|
||||
}
|
||||
|
||||
let response;
|
||||
let responseTime;
|
||||
|
||||
if (request.awsv4config) {
|
||||
// todo: make this happen in prepare-request.js
|
||||
// interpolate the aws v4 config
|
||||
request.awsv4config.accessKeyId = interpolateString(request.awsv4config.accessKeyId, interpolationOptions);
|
||||
request.awsv4config.secretAccessKey = interpolateString(
|
||||
request.awsv4config.secretAccessKey,
|
||||
interpolationOptions
|
||||
);
|
||||
request.awsv4config.sessionToken = interpolateString(request.awsv4config.sessionToken, interpolationOptions);
|
||||
request.awsv4config.service = interpolateString(request.awsv4config.service, interpolationOptions);
|
||||
request.awsv4config.region = interpolateString(request.awsv4config.region, interpolationOptions);
|
||||
request.awsv4config.profileName = interpolateString(request.awsv4config.profileName, interpolationOptions);
|
||||
const isGrpcRequest = item.type === 'grpc-request';
|
||||
const isWsRequest = item.type === 'ws-request';
|
||||
|
||||
request.awsv4config = await resolveAwsV4Credentials(request);
|
||||
addAwsV4Interceptor(axiosInstance, request);
|
||||
delete request.awsv4config;
|
||||
}
|
||||
if (isGrpcRequest) {
|
||||
response = await sendGrpcRequest(item, collection, envVariables, runtimeVariables);
|
||||
responseTime = response.duration || 0;
|
||||
} else if (isWsRequest) {
|
||||
response = await sendWsRequest(item, collection, envVariables, runtimeVariables);
|
||||
responseTime = response.duration || 0;
|
||||
} else {
|
||||
try {
|
||||
let axiosInstance = makeAxiosInstance({
|
||||
requestMaxRedirects: requestMaxRedirects,
|
||||
disableCookies: options.disableCookies
|
||||
});
|
||||
if (request.ntlmConfig) {
|
||||
axiosInstance = NtlmClient(request.ntlmConfig, axiosInstance.defaults);
|
||||
delete request.ntlmConfig;
|
||||
}
|
||||
|
||||
if (request.digestConfig) {
|
||||
addDigestInterceptor(axiosInstance, request);
|
||||
delete request.digestConfig;
|
||||
}
|
||||
if (request.awsv4config) {
|
||||
// todo: make this happen in prepare-request.js
|
||||
// interpolate the aws v4 config
|
||||
request.awsv4config.accessKeyId = interpolateString(request.awsv4config.accessKeyId, interpolationOptions);
|
||||
request.awsv4config.secretAccessKey = interpolateString(
|
||||
request.awsv4config.secretAccessKey,
|
||||
interpolationOptions
|
||||
);
|
||||
request.awsv4config.sessionToken = interpolateString(request.awsv4config.sessionToken, interpolationOptions);
|
||||
request.awsv4config.service = interpolateString(request.awsv4config.service, interpolationOptions);
|
||||
request.awsv4config.region = interpolateString(request.awsv4config.region, interpolationOptions);
|
||||
request.awsv4config.profileName = interpolateString(request.awsv4config.profileName, interpolationOptions);
|
||||
|
||||
/** @type {import('axios').AxiosResponse} */
|
||||
response = await axiosInstance(request);
|
||||
request.awsv4config = await resolveAwsV4Credentials(request);
|
||||
addAwsV4Interceptor(axiosInstance, request);
|
||||
delete request.awsv4config;
|
||||
}
|
||||
|
||||
const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
|
||||
response.data = data;
|
||||
response.dataBuffer = dataBuffer;
|
||||
if (request.digestConfig) {
|
||||
addDigestInterceptor(axiosInstance, request);
|
||||
delete request.digestConfig;
|
||||
}
|
||||
|
||||
// Prevents the duration on leaking to the actual result
|
||||
responseTime = response.headers.get('request-duration');
|
||||
response.headers.delete('request-duration');
|
||||
/** @type {import('axios').AxiosResponse} */
|
||||
response = await axiosInstance(request);
|
||||
|
||||
//save cookies if enabled
|
||||
if (!options.disableCookies) {
|
||||
saveCookies(request.url, response.headers);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.response) {
|
||||
const { data, dataBuffer } = parseDataFromResponse(err?.response);
|
||||
err.response.data = data;
|
||||
err.response.dataBuffer = dataBuffer;
|
||||
response = err.response;
|
||||
const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
|
||||
response.data = data;
|
||||
response.dataBuffer = dataBuffer;
|
||||
|
||||
// Prevents the duration on leaking to the actual result
|
||||
responseTime = response.headers.get('request-duration');
|
||||
response.headers.delete('request-duration');
|
||||
} else {
|
||||
console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`));
|
||||
return {
|
||||
test: {
|
||||
filename: relativeItemPathname
|
||||
},
|
||||
request: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
data: request.data
|
||||
},
|
||||
response: {
|
||||
|
||||
//save cookies if enabled
|
||||
if (!options.disableCookies) {
|
||||
saveCookies(request.url, response.headers);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.response) {
|
||||
const { data, dataBuffer } = parseDataFromResponse(err?.response);
|
||||
err.response.data = data;
|
||||
err.response.dataBuffer = dataBuffer;
|
||||
response = err.response;
|
||||
|
||||
// Prevents the duration on leaking to the actual result
|
||||
responseTime = response.headers.get('request-duration');
|
||||
response.headers.delete('request-duration');
|
||||
} else {
|
||||
console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`));
|
||||
return {
|
||||
test: {
|
||||
filename: relativeItemPathname
|
||||
},
|
||||
request: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
data: request.data
|
||||
},
|
||||
response: {
|
||||
status: 'error',
|
||||
statusText: null,
|
||||
headers: null,
|
||||
data: null,
|
||||
url: null,
|
||||
responseTime: 0
|
||||
},
|
||||
error: err?.message || err?.errors?.map((e) => e?.message)?.at(0) || err?.code || 'Request Failed!',
|
||||
status: 'error',
|
||||
statusText: null,
|
||||
headers: null,
|
||||
data: null,
|
||||
url: null,
|
||||
responseTime: 0
|
||||
},
|
||||
error: err?.message || err?.errors?.map(e => e?.message)?.at(0) || err?.code || 'Request Failed!',
|
||||
status: 'error',
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
preRequestTestResults,
|
||||
postResponseTestResults,
|
||||
nextRequestName: nextRequestName,
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
preRequestTestResults,
|
||||
postResponseTestResults,
|
||||
nextRequestName: nextRequestName,
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,7 +500,7 @@ const runSingleRequest = async function (
|
||||
|
||||
console.log(
|
||||
chalk.green(stripExtension(relativeItemPathname)) +
|
||||
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
|
||||
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
|
||||
);
|
||||
|
||||
// Log pre-request test results
|
||||
@@ -585,7 +603,6 @@ const runSingleRequest = async function (
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
logResults(assertionResults, 'Assertions');
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
const _ = require('lodash');
|
||||
const {
|
||||
parseRequest: _parseRequest,
|
||||
parseCollection: _parseCollection
|
||||
} = require('@usebruno/filestore');
|
||||
const { parseRequest: _parseRequest, parseCollection: _parseCollection } = require('@usebruno/filestore');
|
||||
|
||||
const collectionBruToJson = (bru) => {
|
||||
try {
|
||||
@@ -24,7 +21,7 @@ const collectionBruToJson = (bru) => {
|
||||
const sequence = _.get(json, 'meta.seq');
|
||||
if (json?.meta) {
|
||||
transformedJson.meta = {
|
||||
name: json.meta.name,
|
||||
name: json.meta.name
|
||||
};
|
||||
|
||||
if (sequence) {
|
||||
@@ -63,6 +60,9 @@ const bruToJson = (bru) => {
|
||||
case 'grpc':
|
||||
requestType = 'grpc-request';
|
||||
break;
|
||||
case 'ws':
|
||||
requestType = 'ws-request';
|
||||
break;
|
||||
default:
|
||||
requestType = 'http-request';
|
||||
}
|
||||
@@ -88,18 +88,31 @@ const bruToJson = (bru) => {
|
||||
|
||||
if (requestType === 'grpc-request') {
|
||||
const selectedMethod = _.get(json, 'grpc.method');
|
||||
if(selectedMethod) transformedJson.request.method = selectedMethod;
|
||||
if (selectedMethod) transformedJson.request.method = selectedMethod;
|
||||
const selectedMethodType = _.get(json, 'grpc.methodType');
|
||||
if(selectedMethodType) transformedJson.request.methodType = selectedMethodType;
|
||||
if (selectedMethodType) transformedJson.request.methodType = selectedMethodType;
|
||||
const protoPath = _.get(json, 'grpc.protoPath');
|
||||
if(protoPath) transformedJson.request.protoPath = protoPath;
|
||||
if (protoPath) transformedJson.request.protoPath = protoPath;
|
||||
transformedJson.request.auth.mode = _.get(json, 'grpc.auth', 'none');
|
||||
transformedJson.request.body = _.get(json, 'body', {
|
||||
mode: 'grpc',
|
||||
grpc: [{
|
||||
name: 'message 1',
|
||||
content: '{}'
|
||||
}]
|
||||
grpc: [
|
||||
{
|
||||
name: 'message 1',
|
||||
content: '{}'
|
||||
}
|
||||
]
|
||||
});
|
||||
} else if (requestType === 'ws-request') {
|
||||
transformedJson.request.auth.mode = _.get(json, 'ws.auth', 'none');
|
||||
transformedJson.request.body = _.get(json, 'body', {
|
||||
mode: 'ws',
|
||||
ws: [
|
||||
{
|
||||
name: 'message 1',
|
||||
content: '{}'
|
||||
}
|
||||
]
|
||||
});
|
||||
} else {
|
||||
transformedJson.request.method = _.upperCase(_.get(json, 'http.method'));
|
||||
@@ -108,8 +121,6 @@ const bruToJson = (bru) => {
|
||||
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
|
||||
}
|
||||
|
||||
|
||||
|
||||
return transformedJson;
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
|
||||
@@ -555,7 +555,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
|
||||
fs.rmSync(pathname, { recursive: true, force: true });
|
||||
} else if (['http-request', 'graphql-request', 'grpc-request'].includes(type)) {
|
||||
} else if (['http-request', 'graphql-request', 'grpc-request', 'ws-request'].includes(type)) {
|
||||
if (!fs.existsSync(pathname)) {
|
||||
return Promise.reject(new Error('The file does not exist'));
|
||||
}
|
||||
|
||||
@@ -19,12 +19,31 @@ const { prepareRequest } = require('./prepare-request');
|
||||
const interpolateVars = require('./interpolate-vars');
|
||||
const { makeAxiosInstance } = require('./axios-instance');
|
||||
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
|
||||
const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseDataFromRequest } = require('../../utils/common');
|
||||
const {
|
||||
uuid,
|
||||
safeStringifyJSON,
|
||||
safeParseJSON,
|
||||
parseDataFromResponse,
|
||||
parseDataFromRequest
|
||||
} = require('../../utils/common');
|
||||
const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem');
|
||||
const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies');
|
||||
const { createFormData } = require('../../utils/form-data');
|
||||
const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars, sortByNameThenSequence } = require('../../utils/collection');
|
||||
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant } = require('../../utils/oauth2');
|
||||
const {
|
||||
findItemInCollectionByPathname,
|
||||
sortFolder,
|
||||
getAllRequestsInFolderRecursively,
|
||||
getEnvVars,
|
||||
getTreePathFromCollectionToItem,
|
||||
mergeVars,
|
||||
sortByNameThenSequence
|
||||
} = require('../../utils/collection');
|
||||
const {
|
||||
getOAuth2TokenUsingAuthorizationCode,
|
||||
getOAuth2TokenUsingClientCredentials,
|
||||
getOAuth2TokenUsingPasswordCredentials,
|
||||
getOAuth2TokenUsingImplicitGrant
|
||||
} = require('../../utils/oauth2');
|
||||
const { preferencesUtil } = require('../../store/preferences');
|
||||
const { getProcessEnvVars } = require('../../store/process-env');
|
||||
const { getBrunoConfig } = require('../../store/bruno-config');
|
||||
@@ -32,15 +51,14 @@ const Oauth2Store = require('../../store/oauth2');
|
||||
const { isRequestTagsIncluded } = require('@usebruno/common');
|
||||
const { cookiesStore } = require('../../store/cookies');
|
||||
const registerGrpcEventHandlers = require('./grpc-event-handlers');
|
||||
const { registerWsEventHandlers } = require('./ws-event-handlers');
|
||||
const { getCertsAndProxyConfig } = require('./cert-utils');
|
||||
|
||||
const saveCookies = (url, headers) => {
|
||||
if (preferencesUtil.shouldStoreCookies()) {
|
||||
let setCookieHeaders = [];
|
||||
if (headers['set-cookie']) {
|
||||
setCookieHeaders = Array.isArray(headers['set-cookie'])
|
||||
? headers['set-cookie']
|
||||
: [headers['set-cookie']];
|
||||
setCookieHeaders = Array.isArray(headers['set-cookie']) ? headers['set-cookie'] : [headers['set-cookie']];
|
||||
for (let setCookieHeader of setCookieHeaders) {
|
||||
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
|
||||
addCookieToJar(setCookieHeader, url);
|
||||
@@ -48,21 +66,14 @@ const saveCookies = (url, headers) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getJsSandboxRuntime = (collection) => {
|
||||
const securityConfig = get(collection, 'securityConfig', {});
|
||||
return securityConfig.jsSandboxMode === 'safe' ? 'quickjs' : 'vm2';
|
||||
};
|
||||
|
||||
const configureRequest = async (
|
||||
collectionUid,
|
||||
request,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
collectionPath
|
||||
) => {
|
||||
const configureRequest = async (collectionUid, request, envVars, runtimeVariables, processEnvVars, collectionPath) => {
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
if (!protocolRegex.test(request.url)) {
|
||||
request.url = `http://${request.url}`;
|
||||
@@ -77,9 +88,9 @@ const configureRequest = async (
|
||||
collectionPath
|
||||
});
|
||||
|
||||
let requestMaxRedirects = request.maxRedirects
|
||||
request.maxRedirects = 0
|
||||
|
||||
let requestMaxRedirects = request.maxRedirects;
|
||||
request.maxRedirects = 0;
|
||||
|
||||
// Set default value for requestMaxRedirects if not explicitly set
|
||||
if (requestMaxRedirects === undefined) {
|
||||
requestMaxRedirects = 5; // Default to 5 redirects
|
||||
@@ -95,7 +106,7 @@ const configureRequest = async (
|
||||
});
|
||||
|
||||
if (request.ntlmConfig) {
|
||||
axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults)
|
||||
axiosInstance = NtlmClient(request.ntlmConfig, axiosInstance.defaults);
|
||||
delete request.ntlmConfig;
|
||||
}
|
||||
|
||||
@@ -106,66 +117,106 @@ const configureRequest = async (
|
||||
switch (grantType) {
|
||||
case 'authorization_code':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid, certsAndProxyConfig }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
({
|
||||
credentials,
|
||||
url: oauth2Url,
|
||||
credentialsId,
|
||||
debugInfo
|
||||
} = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid, certsAndProxyConfig }));
|
||||
request.oauth2Credentials = {
|
||||
credentials,
|
||||
url: oauth2Url,
|
||||
collectionUid,
|
||||
credentialsId,
|
||||
debugInfo,
|
||||
folderUid: request.oauth2Credentials?.folderUid
|
||||
};
|
||||
if (tokenPlacement == 'header' && credentials?.access_token) {
|
||||
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials.access_token}`.trim();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
|
||||
request.url = url?.toString();
|
||||
}
|
||||
catch(error) {}
|
||||
} catch (error) {}
|
||||
}
|
||||
break;
|
||||
case 'implicit':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingImplicitGrant({ request: requestCopy, collectionUid }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
({
|
||||
credentials,
|
||||
url: oauth2Url,
|
||||
credentialsId,
|
||||
debugInfo
|
||||
} = await getOAuth2TokenUsingImplicitGrant({ request: requestCopy, collectionUid }));
|
||||
request.oauth2Credentials = {
|
||||
credentials,
|
||||
url: oauth2Url,
|
||||
collectionUid,
|
||||
credentialsId,
|
||||
debugInfo,
|
||||
folderUid: request.oauth2Credentials?.folderUid
|
||||
};
|
||||
if (tokenPlacement == 'header') {
|
||||
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
|
||||
request.url = url?.toString();
|
||||
}
|
||||
catch(error) {}
|
||||
} catch (error) {}
|
||||
}
|
||||
break;
|
||||
case 'client_credentials':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid, certsAndProxyConfig }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
({
|
||||
credentials,
|
||||
url: oauth2Url,
|
||||
credentialsId,
|
||||
debugInfo
|
||||
} = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid, certsAndProxyConfig }));
|
||||
request.oauth2Credentials = {
|
||||
credentials,
|
||||
url: oauth2Url,
|
||||
collectionUid,
|
||||
credentialsId,
|
||||
debugInfo,
|
||||
folderUid: request.oauth2Credentials?.folderUid
|
||||
};
|
||||
if (tokenPlacement == 'header' && credentials?.access_token) {
|
||||
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials.access_token}`.trim();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
|
||||
request.url = url?.toString();
|
||||
}
|
||||
catch(error) {}
|
||||
} catch (error) {}
|
||||
}
|
||||
break;
|
||||
case 'password':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid, certsAndProxyConfig }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
({
|
||||
credentials,
|
||||
url: oauth2Url,
|
||||
credentialsId,
|
||||
debugInfo
|
||||
} = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid, certsAndProxyConfig }));
|
||||
request.oauth2Credentials = {
|
||||
credentials,
|
||||
url: oauth2Url,
|
||||
collectionUid,
|
||||
credentialsId,
|
||||
debugInfo,
|
||||
folderUid: request.oauth2Credentials?.folderUid
|
||||
};
|
||||
if (tokenPlacement == 'header' && credentials?.access_token) {
|
||||
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials.access_token}`.trim();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
|
||||
request.url = url?.toString();
|
||||
}
|
||||
catch(error) {}
|
||||
} catch (error) {}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -187,29 +238,28 @@ const configureRequest = async (
|
||||
if (preferencesUtil.shouldSendCookies()) {
|
||||
const cookieString = getCookieStringForUrl(request.url);
|
||||
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
|
||||
const existingCookieHeaderName = Object.keys(request.headers).find(
|
||||
name => name.toLowerCase() === 'cookie'
|
||||
);
|
||||
const existingCookieHeaderName = Object.keys(request.headers).find((name) => name.toLowerCase() === 'cookie');
|
||||
const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : '';
|
||||
|
||||
|
||||
// Helper function to parse cookies into an object
|
||||
const parseCookies = (str) => str.split(';').reduce((cookies, cookie) => {
|
||||
const parseCookies = (str) =>
|
||||
str.split(';').reduce((cookies, cookie) => {
|
||||
const [name, ...rest] = cookie.split('=');
|
||||
if (name && name.trim()) {
|
||||
cookies[name.trim()] = rest.join('=').trim();
|
||||
cookies[name.trim()] = rest.join('=').trim();
|
||||
}
|
||||
return cookies;
|
||||
}, {});
|
||||
|
||||
}, {});
|
||||
|
||||
const mergedCookies = {
|
||||
...parseCookies(existingCookieString),
|
||||
...parseCookies(cookieString),
|
||||
...parseCookies(existingCookieString),
|
||||
...parseCookies(cookieString)
|
||||
};
|
||||
|
||||
|
||||
const combinedCookieString = Object.entries(mergedCookies)
|
||||
.map(([name, value]) => `${name}=${value}`)
|
||||
.join('; ');
|
||||
|
||||
.map(([name, value]) => `${name}=${value}`)
|
||||
.join('; ');
|
||||
|
||||
request.headers[existingCookieHeaderName || 'Cookie'] = combinedCookieString;
|
||||
}
|
||||
}
|
||||
@@ -317,15 +367,15 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
};
|
||||
|
||||
const notifyScriptExecution = ({
|
||||
channel, // 'main:run-request-event' | 'main:run-folder-event'
|
||||
basePayload, // request-level or runner-level identifiers
|
||||
scriptType, // 'pre-request' | 'post-response' | 'test'
|
||||
error // optional Error
|
||||
channel, // 'main:run-request-event' | 'main:run-folder-event'
|
||||
basePayload, // request-level or runner-level identifiers
|
||||
scriptType, // 'pre-request' | 'post-response' | 'test'
|
||||
error // optional Error
|
||||
}) => {
|
||||
mainWindow.webContents.send(channel, {
|
||||
type: `${scriptType}-script-execution`,
|
||||
...basePayload,
|
||||
errorMessage: error ? (error.message || `An error occurred in ${scriptType.replace('-', ' ')} script`) : null
|
||||
errorMessage: error ? error.message || `An error occurred in ${scriptType.replace('-', ' ')} script` : null
|
||||
});
|
||||
};
|
||||
|
||||
@@ -343,7 +393,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
) => {
|
||||
// run pre-request script
|
||||
let scriptResult;
|
||||
const collectionName = collection?.name
|
||||
const collectionName = collection?.name;
|
||||
const requestScript = get(request, 'script.req');
|
||||
if (requestScript?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
@@ -468,7 +518,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
// run post-response script
|
||||
const responseScript = get(request, 'script.res');
|
||||
let scriptResult;
|
||||
const collectionName = collection?.name
|
||||
const collectionName = collection?.name;
|
||||
if (responseScript?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
scriptResult = await scriptRuntime.runResponseScript(
|
||||
@@ -509,7 +559,14 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
return scriptResult;
|
||||
};
|
||||
|
||||
const runRequest = async ({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground = false }) => {
|
||||
const runRequest = async ({
|
||||
item,
|
||||
collection,
|
||||
envVars,
|
||||
processEnvVars,
|
||||
runtimeVariables,
|
||||
runInBackground = false
|
||||
}) => {
|
||||
const collectionUid = collection.uid;
|
||||
const collectionPath = collection.pathname;
|
||||
const cancelTokenUid = uuid();
|
||||
@@ -523,21 +580,29 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
itemPathname = `${itemPathname}.bru`;
|
||||
}
|
||||
const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname));
|
||||
if(_item) {
|
||||
const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true });
|
||||
if (_item) {
|
||||
const res = await runRequest({
|
||||
item: _item,
|
||||
collection,
|
||||
envVars,
|
||||
processEnvVars,
|
||||
runtimeVariables,
|
||||
runInBackground: true
|
||||
});
|
||||
resolve(res);
|
||||
}
|
||||
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'request-queued',
|
||||
requestUid,
|
||||
collectionUid,
|
||||
itemUid: item.uid,
|
||||
cancelTokenUid
|
||||
});
|
||||
!runInBackground &&
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'request-queued',
|
||||
requestUid,
|
||||
collectionUid,
|
||||
itemUid: item.uid,
|
||||
cancelTokenUid
|
||||
});
|
||||
|
||||
const abortController = new AbortController();
|
||||
const request = await prepareRequest(item, collection, abortController);
|
||||
@@ -579,12 +644,13 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
}
|
||||
|
||||
!runInBackground && notifyScriptExecution({
|
||||
channel: 'main:run-request-event',
|
||||
basePayload: { requestUid, collectionUid, itemUid: item.uid },
|
||||
scriptType: 'pre-request',
|
||||
error: preRequestError
|
||||
});
|
||||
!runInBackground &&
|
||||
notifyScriptExecution({
|
||||
channel: 'main:run-request-event',
|
||||
basePayload: { requestUid, collectionUid, itemUid: item.uid },
|
||||
scriptType: 'pre-request',
|
||||
error: preRequestError
|
||||
});
|
||||
|
||||
if (preRequestError) {
|
||||
return Promise.reject(preRequestError);
|
||||
@@ -605,16 +671,17 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
headers: request.headers,
|
||||
data: requestData,
|
||||
dataBuffer: requestDataBuffer
|
||||
}
|
||||
};
|
||||
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'request-sent',
|
||||
requestSent,
|
||||
collectionUid,
|
||||
itemUid: item.uid,
|
||||
requestUid,
|
||||
cancelTokenUid
|
||||
});
|
||||
!runInBackground &&
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'request-sent',
|
||||
requestSent,
|
||||
collectionUid,
|
||||
itemUid: item.uid,
|
||||
requestUid,
|
||||
cancelTokenUid
|
||||
});
|
||||
|
||||
if (request?.oauth2Credentials) {
|
||||
mainWindow.webContents.send('main:credentials-update', {
|
||||
@@ -622,8 +689,10 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
url: request?.oauth2Credentials?.url,
|
||||
collectionUid,
|
||||
credentialsId: request?.oauth2Credentials?.credentialsId,
|
||||
...(request?.oauth2Credentials?.folderUid ? { folderUid: request.oauth2Credentials.folderUid } : { itemUid: item.uid }),
|
||||
debugInfo: request?.oauth2Credentials?.debugInfo,
|
||||
...(request?.oauth2Credentials?.folderUid
|
||||
? { folderUid: request.oauth2Credentials.folderUid }
|
||||
: { itemUid: item.uid }),
|
||||
debugInfo: request?.oauth2Credentials?.debugInfo
|
||||
});
|
||||
}
|
||||
|
||||
@@ -665,7 +734,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
statusText: error.statusText,
|
||||
error: error.message || 'Error occured while executing the request!',
|
||||
timeline: error.timeline
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,7 +777,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
console.error('Post-response script error:', error);
|
||||
postResponseError = error;
|
||||
}
|
||||
|
||||
|
||||
if (postResponseScriptResult?.results) {
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'test-results-post-response',
|
||||
@@ -719,12 +788,13 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
}
|
||||
|
||||
!runInBackground && notifyScriptExecution({
|
||||
channel: 'main:run-request-event',
|
||||
basePayload: { requestUid, collectionUid, itemUid: item.uid },
|
||||
scriptType: 'post-response',
|
||||
error: postResponseError
|
||||
});
|
||||
!runInBackground &&
|
||||
notifyScriptExecution({
|
||||
channel: 'main:run-request-event',
|
||||
basePayload: { requestUid, collectionUid, itemUid: item.uid },
|
||||
scriptType: 'post-response',
|
||||
error: postResponseError
|
||||
});
|
||||
|
||||
// run assertions
|
||||
const assertions = get(request, 'assertions');
|
||||
@@ -739,17 +809,18 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'assertion-results',
|
||||
results: results,
|
||||
itemUid: item.uid,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
!runInBackground &&
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'assertion-results',
|
||||
results: results,
|
||||
itemUid: item.uid,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
const testFile = get(request, 'tests');
|
||||
const collectionName = collection?.name
|
||||
const collectionName = collection?.name;
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
let testResults = null;
|
||||
@@ -771,7 +842,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
);
|
||||
} catch (error) {
|
||||
testError = error;
|
||||
|
||||
|
||||
if (error.partialResults) {
|
||||
testResults = error.partialResults;
|
||||
} else {
|
||||
@@ -786,13 +857,14 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
}
|
||||
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'test-results',
|
||||
results: testResults.results,
|
||||
itemUid: item.uid,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
!runInBackground &&
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'test-results',
|
||||
results: testResults.results,
|
||||
itemUid: item.uid,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: testResults.envVariables,
|
||||
@@ -812,12 +884,13 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables;
|
||||
|
||||
!runInBackground && notifyScriptExecution({
|
||||
channel: 'main:run-request-event',
|
||||
basePayload: { requestUid, collectionUid, itemUid: item.uid },
|
||||
scriptType: 'test',
|
||||
error: testError
|
||||
});
|
||||
!runInBackground &&
|
||||
notifyScriptExecution({
|
||||
channel: 'main:run-request-event',
|
||||
basePayload: { requestUid, collectionUid, itemUid: item.uid },
|
||||
scriptType: 'test',
|
||||
error: testError
|
||||
});
|
||||
|
||||
const domainsWithCookiesTest = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest)));
|
||||
@@ -846,7 +919,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
timeline: error?.timeline
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// handler for sending http request
|
||||
ipcMain.handle('send-http-request', async (event, item, collection, environment, runtimeVariables) => {
|
||||
@@ -881,7 +954,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
// handler for fetch-gql-schema
|
||||
ipcMain.handle('fetch-gql-schema', fetchGqlSchemaHandler)
|
||||
ipcMain.handle('fetch-gql-schema', fetchGqlSchemaHandler);
|
||||
|
||||
ipcMain.handle(
|
||||
'renderer:run-collection-folder',
|
||||
@@ -907,13 +980,20 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
itemPathname = `${itemPathname}.bru`;
|
||||
}
|
||||
const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname));
|
||||
if(_item) {
|
||||
const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true });
|
||||
if (_item) {
|
||||
const res = await runRequest({
|
||||
item: _item,
|
||||
collection,
|
||||
envVars,
|
||||
processEnvVars,
|
||||
runtimeVariables,
|
||||
runInBackground: true
|
||||
});
|
||||
resolve(res);
|
||||
}
|
||||
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!folder) {
|
||||
folder = collection;
|
||||
@@ -940,9 +1020,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// sort requests by seq property
|
||||
folderRequests = sortByNameThenSequence(folderRequests)
|
||||
folderRequests = sortByNameThenSequence(folderRequests);
|
||||
}
|
||||
|
||||
// Filter requests based on tags
|
||||
@@ -951,7 +1030,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const excludeTags = tags.exclude ? tags.exclude : [];
|
||||
folderRequests = folderRequests.filter(({ tags: requestTags = [], draft }) => {
|
||||
requestTags = draft?.tags || requestTags || [];
|
||||
return isRequestTagsIncluded(requestTags, includeTags, excludeTags)
|
||||
return isRequestTagsIncluded(requestTags, includeTags, excludeTags);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1004,7 +1083,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
const request = await prepareRequest(item, collection, abortController);
|
||||
request.__bruno__executionMode = 'runner';
|
||||
|
||||
|
||||
const requestUid = uuid();
|
||||
|
||||
try {
|
||||
@@ -1044,7 +1123,10 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
const domainsWithCookiesPreRequest = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPreRequest)));
|
||||
mainWindow.webContents.send(
|
||||
'main:cookies-update',
|
||||
safeParseJSON(safeStringifyJSON(domainsWithCookiesPreRequest))
|
||||
);
|
||||
|
||||
if (preRequestError) {
|
||||
throw preRequestError;
|
||||
@@ -1082,7 +1164,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
headers: request.headers,
|
||||
data: requestData,
|
||||
dataBuffer: requestDataBuffer
|
||||
}
|
||||
};
|
||||
|
||||
// todo:
|
||||
// i have no clue why electron can't send the request object
|
||||
@@ -1109,8 +1191,10 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
url: request?.oauth2Credentials?.url,
|
||||
collectionUid,
|
||||
credentialsId: request?.oauth2Credentials?.credentialsId,
|
||||
...(request?.oauth2Credentials?.folderUid ? { folderUid: request.oauth2Credentials.folderUid } : { itemUid: item.uid }),
|
||||
debugInfo: request?.oauth2Credentials?.debugInfo,
|
||||
...(request?.oauth2Credentials?.folderUid
|
||||
? { folderUid: request.oauth2Credentials.folderUid }
|
||||
: { itemUid: item.uid }),
|
||||
debugInfo: request?.oauth2Credentials?.debugInfo
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1161,7 +1245,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
data: response.data,
|
||||
responseTime: response.responseTime,
|
||||
timeline: response.timeline,
|
||||
url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null
|
||||
url: response.request
|
||||
? response.request.protocol + '//' + response.request.host + response.request.path
|
||||
: null
|
||||
},
|
||||
...eventData
|
||||
});
|
||||
@@ -1188,7 +1274,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
size: Buffer.byteLength(dataBuffer),
|
||||
data: error.response.data,
|
||||
responseTime: error.response.responseTime,
|
||||
timeline: error.response.timeline,
|
||||
timeline: error.response.timeline
|
||||
};
|
||||
|
||||
// if we get a response from the server, we consider it as a success
|
||||
@@ -1235,7 +1321,10 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
const domainsWithCookiesPostResponse = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPostResponse)));
|
||||
mainWindow.webContents.send(
|
||||
'main:cookies-update',
|
||||
safeParseJSON(safeStringifyJSON(domainsWithCookiesPostResponse))
|
||||
);
|
||||
|
||||
if (postResponseScriptResult?.nextRequestName !== undefined) {
|
||||
nextRequestName = postResponseScriptResult.nextRequestName;
|
||||
@@ -1276,7 +1365,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
const testFile = get(request, 'tests');
|
||||
const collectionName = collection?.name
|
||||
const collectionName = collection?.name;
|
||||
if (typeof testFile === 'string') {
|
||||
let testResults = null;
|
||||
let testError = null;
|
||||
@@ -1298,7 +1387,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
);
|
||||
} catch (error) {
|
||||
testError = error;
|
||||
|
||||
|
||||
if (error.partialResults) {
|
||||
testResults = error.partialResults;
|
||||
} else {
|
||||
@@ -1332,7 +1421,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: testResults.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
|
||||
collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables;
|
||||
|
||||
notifyScriptExecution({
|
||||
@@ -1343,7 +1432,10 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
const domainsWithCookiesTest = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest)));
|
||||
mainWindow.webContents.send(
|
||||
'main:cookies-update',
|
||||
safeParseJSON(safeStringifyJSON(domainsWithCookiesTest))
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
@@ -1392,7 +1484,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
folderUid
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
console.log('error', error);
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'testrun-ended',
|
||||
@@ -1423,7 +1515,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
try {
|
||||
const disposition = contentDispositionParser.parse(contentDisposition);
|
||||
return disposition && disposition.parameters['filename'];
|
||||
} catch (error) { }
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
const getFileNameFromUrlPath = () => {
|
||||
@@ -1455,7 +1547,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const filePath = await chooseFileToSave(mainWindow, fileName);
|
||||
if (filePath) {
|
||||
const encoding = getEncodingFormat();
|
||||
const data = Buffer.from(response.dataBuffer, 'base64')
|
||||
const data = Buffer.from(response.dataBuffer, 'base64');
|
||||
if (encoding === 'utf-8') {
|
||||
await writeFile(filePath, data);
|
||||
} else {
|
||||
@@ -1483,17 +1575,19 @@ const executeRequestOnFailHandler = async (request, error) => {
|
||||
} catch (handlerError) {
|
||||
console.error('Error executing onFail handler', handlerError);
|
||||
// @TODO: This is a temporary solution to display the error message in the response pane. Revisit and handle properly.
|
||||
error.message = `1. Request failed: ${error.message || 'Error occured while executing the request!'}\n2. Error executing onFail handler: ${handlerError.message || 'Unknown error'}`;
|
||||
error.message = `1. Request failed: ${
|
||||
error.message || 'Error occured while executing the request!'
|
||||
}\n2. Error executing onFail handler: ${handlerError.message || 'Unknown error'}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const registerAllNetworkIpc = (mainWindow) => {
|
||||
registerNetworkIpc(mainWindow);
|
||||
registerGrpcEventHandlers(mainWindow);
|
||||
}
|
||||
registerWsEventHandlers(mainWindow);
|
||||
};
|
||||
|
||||
module.exports = registerAllNetworkIpc
|
||||
module.exports = registerAllNetworkIpc;
|
||||
module.exports.configureRequest = configureRequest;
|
||||
module.exports.getCertsAndProxyConfig = getCertsAndProxyConfig;
|
||||
module.exports.fetchGqlSchemaHandler = fetchGqlSchemaHandler;
|
||||
|
||||
378
packages/bruno-electron/src/ipc/network/ws-event-handlers.js
Normal file
378
packages/bruno-electron/src/ipc/network/ws-event-handlers.js
Normal file
@@ -0,0 +1,378 @@
|
||||
const { ipcMain, app } = require('electron');
|
||||
const { WsClient } = require('@usebruno/requests');
|
||||
const { safeParseJSON, safeStringifyJSON } = require('../../utils/common');
|
||||
const { cloneDeep, each, get } = require('lodash');
|
||||
const interpolateVars = require('./interpolate-vars');
|
||||
const { preferencesUtil } = require('../../store/preferences');
|
||||
const { getCertsAndProxyConfig } = require('./cert-utils');
|
||||
const {
|
||||
getEnvVars,
|
||||
getTreePathFromCollectionToItem,
|
||||
mergeHeaders,
|
||||
mergeScripts,
|
||||
mergeVars,
|
||||
mergeAuth,
|
||||
getFormattedCollectionOauth2Credentials
|
||||
} = require('../../utils/collection');
|
||||
const { getProcessEnvVars } = require('../../store/process-env');
|
||||
const {
|
||||
getOAuth2TokenUsingPasswordCredentials,
|
||||
getOAuth2TokenUsingClientCredentials,
|
||||
getOAuth2TokenUsingAuthorizationCode
|
||||
} = require('../../utils/oauth2');
|
||||
const { interpolateString } = require('./interpolate-string');
|
||||
const path = require('node:path');
|
||||
|
||||
const setWsAuthHeaders = (wsRequest, request, collectionRoot) => {
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
if (collectionAuth && request.auth?.mode === 'inherit') {
|
||||
if (collectionAuth.mode === 'basic') {
|
||||
wsRequest.basicAuth = {
|
||||
username: get(collectionAuth, 'basic.username'),
|
||||
password: get(collectionAuth, 'basic.password')
|
||||
};
|
||||
}
|
||||
|
||||
if (collectionAuth.mode === 'bearer') {
|
||||
wsRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`;
|
||||
}
|
||||
|
||||
if (collectionAuth.mode === 'apikey') {
|
||||
wsRequest.headers[collectionAuth.apikey?.key] = collectionAuth.apikey?.value;
|
||||
}
|
||||
|
||||
if (collectionAuth.mode === 'oauth2') {
|
||||
const grantType = get(collectionAuth, 'oauth2.grantType');
|
||||
|
||||
if (grantType === 'client_credentials') {
|
||||
wsRequest.oauth2 = {
|
||||
grantType,
|
||||
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
|
||||
clientId: get(collectionAuth, 'oauth2.clientId'),
|
||||
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
|
||||
scope: get(collectionAuth, 'oauth2.scope'),
|
||||
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
|
||||
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
|
||||
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey')
|
||||
};
|
||||
} else if (grantType === 'password') {
|
||||
wsRequest.oauth2 = {
|
||||
grantType,
|
||||
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
|
||||
username: get(collectionAuth, 'oauth2.username'),
|
||||
password: get(collectionAuth, 'oauth2.password'),
|
||||
clientId: get(collectionAuth, 'oauth2.clientId'),
|
||||
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
|
||||
scope: get(collectionAuth, 'oauth2.scope'),
|
||||
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
|
||||
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
|
||||
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey')
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (request.auth && request.auth.mode !== 'inherit') {
|
||||
if (request.auth.mode === 'basic') {
|
||||
wsRequest.basicAuth = {
|
||||
username: get(request, 'auth.basic.username'),
|
||||
password: get(request, 'auth.basic.password')
|
||||
};
|
||||
}
|
||||
|
||||
if (request.auth.mode === 'bearer') {
|
||||
wsRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
|
||||
}
|
||||
|
||||
if (request.auth.mode === 'apikey') {
|
||||
wsRequest.headers[get(request, 'auth.apikey.key')] = get(request, 'auth.apikey.value');
|
||||
}
|
||||
|
||||
if (request.auth.mode === 'oauth2') {
|
||||
const grantType = get(request, 'auth.oauth2.grantType');
|
||||
|
||||
if (grantType === 'client_credentials') {
|
||||
wsRequest.oauth2 = {
|
||||
grantType,
|
||||
clientId: get(request, 'auth.oauth2.clientId'),
|
||||
clientSecret: get(request, 'auth.oauth2.clientSecret'),
|
||||
scope: get(request, 'auth.oauth2.scope'),
|
||||
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
|
||||
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
|
||||
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
|
||||
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey')
|
||||
};
|
||||
} else if (grantType === 'password') {
|
||||
wsRequest.oauth2 = {
|
||||
grantType,
|
||||
username: get(request, 'auth.oauth2.username'),
|
||||
password: get(request, 'auth.oauth2.password'),
|
||||
clientId: get(request, 'auth.oauth2.clientId'),
|
||||
clientSecret: get(request, 'auth.oauth2.clientSecret'),
|
||||
scope: get(request, 'auth.oauth2.scope'),
|
||||
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
|
||||
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
|
||||
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
|
||||
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey')
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wsRequest;
|
||||
};
|
||||
|
||||
const prepareWsRequest = async (item, collection, environment, runtimeVariables, certsAndProxyConfig = {}) => {
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
|
||||
const envVars = getEnvVars(environment);
|
||||
const processEnvVars = getProcessEnvVars();
|
||||
|
||||
let wsRequest = {
|
||||
uid: item.uid,
|
||||
url: request.url,
|
||||
headers: request.headers || [],
|
||||
processEnvVars,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
body: request.body,
|
||||
// Add variable properties for interpolation
|
||||
vars: request.vars,
|
||||
collectionVariables: request.collectionVariables,
|
||||
folderVariables: request.folderVariables,
|
||||
requestVariables: request.requestVariables,
|
||||
globalEnvironmentVariables: request.globalEnvironmentVariables,
|
||||
oauth2CredentialVariables: request.oauth2CredentialVariables
|
||||
};
|
||||
|
||||
wsRequest = setWsAuthHeaders(wsRequest, request, collection);
|
||||
|
||||
if (wsRequest.oauth2) {
|
||||
let requestCopy = cloneDeep(wsRequest);
|
||||
const { oauth2: { grantType, tokenPlacement, tokenHeaderPrefix, tokenQueryKey } = {} } = requestCopy || {};
|
||||
let credentials, credentialsId, oauth2Url, debugInfo;
|
||||
|
||||
switch (grantType) {
|
||||
case 'authorization_code':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({
|
||||
credentials,
|
||||
url: oauth2Url,
|
||||
credentialsId,
|
||||
debugInfo
|
||||
} = await getOAuth2TokenUsingAuthorizationCode({
|
||||
request: requestCopy,
|
||||
collectionUid: collection.uid,
|
||||
certsAndProxyConfig
|
||||
}));
|
||||
wsRequest.oauth2Credentials = {
|
||||
credentials,
|
||||
url: oauth2Url,
|
||||
collectionUid: collection.uid,
|
||||
credentialsId,
|
||||
debugInfo,
|
||||
folderUid: request.oauth2Credentials?.folderUid
|
||||
};
|
||||
if (tokenPlacement == 'header') {
|
||||
wsRequest.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;
|
||||
} else {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
|
||||
request.url = url?.toString();
|
||||
} catch (error) {}
|
||||
}
|
||||
break;
|
||||
case 'client_credentials':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({
|
||||
credentials,
|
||||
url: oauth2Url,
|
||||
credentialsId,
|
||||
debugInfo
|
||||
} = await getOAuth2TokenUsingClientCredentials({
|
||||
request: requestCopy,
|
||||
collectionUid: collection.uid,
|
||||
certsAndProxyConfig
|
||||
}));
|
||||
wsRequest.oauth2Credentials = {
|
||||
credentials,
|
||||
url: oauth2Url,
|
||||
collectionUid: collection.uid,
|
||||
credentialsId,
|
||||
debugInfo,
|
||||
folderUid: request.oauth2Credentials?.folderUid
|
||||
};
|
||||
if (tokenPlacement == 'header') {
|
||||
wsRequest.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;
|
||||
} else {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
|
||||
request.url = url?.toString();
|
||||
} catch (error) {}
|
||||
}
|
||||
break;
|
||||
case 'password':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({
|
||||
credentials,
|
||||
url: oauth2Url,
|
||||
credentialsId,
|
||||
debugInfo
|
||||
} = await getOAuth2TokenUsingPasswordCredentials({
|
||||
request: requestCopy,
|
||||
collectionUid: collection.uid,
|
||||
certsAndProxyConfig
|
||||
}));
|
||||
wsRequest.oauth2Credentials = {
|
||||
credentials,
|
||||
url: oauth2Url,
|
||||
collectionUid: collection.uid,
|
||||
credentialsId,
|
||||
debugInfo,
|
||||
folderUid: request.oauth2Credentials?.folderUid
|
||||
};
|
||||
if (tokenPlacement == 'header') {
|
||||
wsRequest.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;
|
||||
} else {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
|
||||
request.url = url?.toString();
|
||||
} catch (error) {}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
interpolateVars(wsRequest, envVars, runtimeVariables, processEnvVars);
|
||||
|
||||
return wsRequest;
|
||||
};
|
||||
|
||||
// Creating wsClient at module level so it can be accessed from window-all-closed event
|
||||
let wsClient;
|
||||
|
||||
/**
|
||||
* Register IPC handlers for WebSocket
|
||||
*/
|
||||
const registerWsEventHandlers = (window) => {
|
||||
const sendEvent = (eventName, ...args) => {
|
||||
if (window && window.webContents) {
|
||||
window.webContents.send(eventName, ...args);
|
||||
} else {
|
||||
console.warn(`Unable to send message "${eventName}": Window not available`);
|
||||
}
|
||||
};
|
||||
|
||||
wsClient = new WsClient(sendEvent);
|
||||
|
||||
ipcMain.handle('ws:connections-changed', (event) => {
|
||||
sendEvent('ws:connections-changed', event);
|
||||
});
|
||||
|
||||
// Start a new WebSocket connection
|
||||
ipcMain.handle('ws:start-connection', async (event, { request, collection, environment, runtimeVariables }) => {
|
||||
try {
|
||||
const requestCopy = cloneDeep(request);
|
||||
const preparedRequest = await prepareWsRequest(requestCopy, collection, environment, runtimeVariables, {});
|
||||
|
||||
const requestSent = {
|
||||
type: 'request',
|
||||
url: preparedRequest.url,
|
||||
headers: preparedRequest.headers,
|
||||
body: preparedRequest.body,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// Start WebSocket connection
|
||||
await wsClient.startConnection({
|
||||
request: preparedRequest,
|
||||
collection,
|
||||
options: {
|
||||
timeout: 30000,
|
||||
keepAlive: true
|
||||
}
|
||||
});
|
||||
|
||||
sendEvent('ws:request', preparedRequest.uid, collection.uid, requestSent);
|
||||
|
||||
// Send OAuth credentials update if available
|
||||
if (preparedRequest?.oauth2Credentials) {
|
||||
window.webContents.send('main:credentials-update', {
|
||||
credentials: preparedRequest.oauth2Credentials?.credentials,
|
||||
url: preparedRequest.oauth2Credentials?.url,
|
||||
collectionUid: collection.uid,
|
||||
credentialsId: preparedRequest.oauth2Credentials?.credentialsId,
|
||||
...(preparedRequest.oauth2Credentials?.folderUid
|
||||
? { folderUid: preparedRequest.oauth2Credentials.folderUid }
|
||||
: { itemUid: preparedRequest.uid }),
|
||||
debugInfo: preparedRequest.oauth2Credentials.debugInfo
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error starting WebSocket connection:', error);
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
}
|
||||
sendEvent('ws:error', request.uid, collection.uid, { error: error.message });
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Get all active connection IDs
|
||||
ipcMain.handle('ws:get-active-connections', (event) => {
|
||||
try {
|
||||
const activeConnectionIds = wsClient.getActiveConnectionIds();
|
||||
return { success: true, activeConnectionIds };
|
||||
} catch (error) {
|
||||
console.error('Error getting active connections:', error);
|
||||
return { success: false, error: error.message, activeConnectionIds: [] };
|
||||
}
|
||||
});
|
||||
|
||||
// Send a message to an existing WebSocket connection
|
||||
ipcMain.handle('ws:send-message', (event, requestId, collectionUid, message) => {
|
||||
try {
|
||||
wsClient.sendMessage(requestId, collectionUid, message);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error sending WebSocket message:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Close a WebSocket connection
|
||||
ipcMain.handle('ws:close-connection', (event, requestId, code, reason) => {
|
||||
try {
|
||||
wsClient.close(requestId, code, reason);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error closing WebSocket connection:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Check if a WebSocket connection is active
|
||||
ipcMain.handle('ws:is-connection-active', (event, requestId) => {
|
||||
try {
|
||||
const isActive = wsClient.isConnectionActive(requestId);
|
||||
return { success: true, isActive };
|
||||
} catch (error) {
|
||||
console.error('Error checking WebSocket connection status:', error);
|
||||
return { success: false, error: error.message, isActive: false };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
registerWsEventHandlers,
|
||||
wsClient
|
||||
};
|
||||
@@ -24,11 +24,19 @@ export const bruRequestToJson = (data: string | any, parsed: boolean = false): a
|
||||
case 'grpc':
|
||||
requestType = 'grpc-request';
|
||||
break;
|
||||
case 'ws':
|
||||
requestType = 'ws-request';
|
||||
break;
|
||||
default:
|
||||
requestType = 'http-request';
|
||||
}
|
||||
|
||||
const sequence = _.get(json, 'meta.seq');
|
||||
const urlPath: Record<typeof requestType, string> = {
|
||||
'grpc-request': 'grpc.url',
|
||||
'ws-request': 'ws.url',
|
||||
default: 'http.url'
|
||||
};
|
||||
const transformedJson = {
|
||||
type: requestType,
|
||||
name: _.get(json, 'meta.name'),
|
||||
@@ -38,7 +46,7 @@ export const bruRequestToJson = (data: string | any, parsed: boolean = false): a
|
||||
request: {
|
||||
method:
|
||||
requestType === 'grpc-request' ? _.get(json, 'grpc.method', '') : _.upperCase(_.get(json, 'http.method')),
|
||||
url: _.get(json, requestType === 'grpc-request' ? 'grpc.url' : 'http.url'),
|
||||
url: _.get(json, urlPath[requestType], urlPath.default),
|
||||
headers: requestType === 'grpc-request' ? _.get(json, 'metadata', []) : _.get(json, 'headers', []),
|
||||
auth: _.get(json, 'auth', {}),
|
||||
body: _.get(json, 'body', {}),
|
||||
@@ -66,6 +74,17 @@ export const bruRequestToJson = (data: string | any, parsed: boolean = false): a
|
||||
}
|
||||
])
|
||||
});
|
||||
} else if (requestType === 'ws-request') {
|
||||
transformedJson.request.auth.mode = _.get(json, 'grpc.auth', 'none');
|
||||
transformedJson.request.body = _.get(json, 'body', {
|
||||
mode: 'ws',
|
||||
grpc: _.get(json, 'body.ws', [
|
||||
{
|
||||
name: 'message 1',
|
||||
content: '{}'
|
||||
}
|
||||
])
|
||||
});
|
||||
} else {
|
||||
// For HTTP and GraphQL
|
||||
(transformedJson.request as any).params = _.get(json, 'params', []);
|
||||
@@ -102,6 +121,9 @@ export const jsonRequestToBru = (json: any): string => {
|
||||
case 'grpc-request':
|
||||
type = 'grpc';
|
||||
break;
|
||||
case 'ws-request':
|
||||
type = 'ws';
|
||||
break;
|
||||
default:
|
||||
type = 'http';
|
||||
}
|
||||
@@ -154,10 +176,29 @@ export const jsonRequestToBru = (json: any): string => {
|
||||
}
|
||||
])
|
||||
});
|
||||
} else if (type === 'ws') {
|
||||
bruJson.ws = {
|
||||
url: _.get(json, 'request.url'),
|
||||
auth: _.get(json, 'request.auth.mode', 'none'),
|
||||
body: _.get(json, 'request.body.mode', 'ws')
|
||||
};
|
||||
const method = _.get(json, 'request.method');
|
||||
const methodType = _.get(json, 'request.methodType');
|
||||
if (method) bruJson.ws.method = method;
|
||||
if (methodType) bruJson.ws.methodType = methodType;
|
||||
bruJson.body = _.get(json, 'request.body', {
|
||||
mode: 'ws',
|
||||
ws: _.get(json, 'request.body.ws', [
|
||||
{
|
||||
name: 'message 1',
|
||||
content: '{}'
|
||||
}
|
||||
])
|
||||
});
|
||||
}
|
||||
|
||||
// Common fields for all request types
|
||||
if (type === 'grpc') {
|
||||
if (type === 'grpc' || type === 'ws') {
|
||||
bruJson.metadata = _.get(json, 'request.headers', []); // Use metadata for gRPC
|
||||
} else {
|
||||
bruJson.headers = _.get(json, 'request.headers', []); // Use headers for HTTP/GraphQL
|
||||
|
||||
@@ -40,6 +40,8 @@ const bruToJson = (fileContents) => {
|
||||
|
||||
const parsed = parser.run(fileContents).result.reduce((acc, item) => _.merge(acc, item), {});
|
||||
|
||||
console.log({ parsed });
|
||||
|
||||
const json = {
|
||||
type: parsed.type || '',
|
||||
name: parsed.name || '',
|
||||
|
||||
@@ -29,9 +29,9 @@ const { safeParseJson, outdentString } = require('./utils');
|
||||
*
|
||||
*/
|
||||
const grammar = ohm.grammar(`Bru {
|
||||
BruFile = (meta | http | grpc | query | params | headers | metadata | auths | bodies | varsandassert | script | tests | settings | docs)*
|
||||
BruFile = (meta | http | grpc | ws | query | params | headers | metadata | auths | bodies | varsandassert | script | tests | settings | docs)*
|
||||
auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey | authOauth2Configs
|
||||
bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body | bodygrpc
|
||||
bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body | bodygrpc | bodyws
|
||||
bodyforms = bodyformurlencoded | bodymultipart | bodyfile
|
||||
params = paramspath | paramsquery
|
||||
|
||||
@@ -82,6 +82,7 @@ const grammar = ohm.grammar(`Bru {
|
||||
|
||||
http = get | post | put | delete | patch | options | head | connect | trace
|
||||
grpc = "grpc" dictionary
|
||||
ws = "ws" dictionary
|
||||
|
||||
get = "get" dictionary
|
||||
post = "post" dictionary
|
||||
@@ -132,6 +133,7 @@ const grammar = ohm.grammar(`Bru {
|
||||
bodygraphql = "body:graphql" st* "{" nl* textblock tagend
|
||||
bodygraphqlvars = "body:graphql:vars" st* "{" nl* textblock tagend
|
||||
bodygrpc = "body:grpc" dictionary
|
||||
bodyws = "body:ws" dictionary
|
||||
|
||||
bodyformurlencoded = "body:form-urlencoded" dictionary
|
||||
bodymultipart = "body:multipart-form" dictionary
|
||||
@@ -219,7 +221,6 @@ const fileExtractContentType = (pair) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => {
|
||||
const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
|
||||
|
||||
@@ -243,10 +244,10 @@ const mapPairListToKeyValPairsFile = (pairList = [], parseEnabled = true) => {
|
||||
fileExtractContentType(pair);
|
||||
|
||||
if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) {
|
||||
let filePath = pair.value.replace(/^@file\(/, '').replace(/\)$/, '');
|
||||
let filePath = pair.value.replace(/^@file\(/, '').replace(/\)$/, '');
|
||||
pair.filePath = filePath;
|
||||
pair.selected = pair.enabled
|
||||
|
||||
pair.selected = pair.enabled;
|
||||
|
||||
// Remove pair.value as it only contains the file path reference
|
||||
delete pair.value;
|
||||
// Remove pair.name as it is auto-generated (e.g., file1, file2, file3, etc.)
|
||||
@@ -338,10 +339,10 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
return chars.sourceString ? chars.sourceString.trim() : '';
|
||||
},
|
||||
list(_1, _2, _3, listitems, _4, _5, _6, _7) {
|
||||
return listitems.ast.flat()
|
||||
return listitems.ast.flat();
|
||||
},
|
||||
listitems(listitem, _1, rest) {
|
||||
return [listitem.ast, ...rest.ast]
|
||||
return [listitem.ast, ...rest.ast];
|
||||
},
|
||||
listitem(_1, textchar, _2) {
|
||||
return textchar.sourceString;
|
||||
@@ -403,6 +404,11 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
grpc: mapPairListToKeyValPair(dictionary.ast)
|
||||
};
|
||||
},
|
||||
ws(_1, dictionary) {
|
||||
return {
|
||||
ws: mapPairListToKeyValPair(dictionary.ast)
|
||||
};
|
||||
},
|
||||
get(_1, dictionary) {
|
||||
return {
|
||||
http: {
|
||||
@@ -580,7 +586,7 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
authOAuth2(_1, dictionary) {
|
||||
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||
const grantTypeKey = _.find(auth, { name: 'grant_type' });
|
||||
@@ -671,7 +677,7 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
|
||||
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : '',
|
||||
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
|
||||
autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true,
|
||||
autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true
|
||||
}
|
||||
: {}
|
||||
}
|
||||
@@ -902,33 +908,74 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
const pairs = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||
const namePair = _.find(pairs, { name: 'name' });
|
||||
const contentPair = _.find(pairs, { name: 'content' });
|
||||
|
||||
|
||||
const messageName = namePair ? namePair.value : '';
|
||||
const messageContent = contentPair ? contentPair.value : '';
|
||||
|
||||
|
||||
try {
|
||||
// Validate JSON by parsing (but don't modify the original string)
|
||||
JSON.parse(messageContent);
|
||||
} catch (error) {
|
||||
console.error("Error validating gRPC message JSON:", error);
|
||||
console.error('Error validating gRPC message JSON:', error);
|
||||
return {
|
||||
body: {
|
||||
mode: 'grpc',
|
||||
grpc: [{
|
||||
name: messageName,
|
||||
content: '{}'
|
||||
}]
|
||||
grpc: [
|
||||
{
|
||||
name: messageName,
|
||||
content: '{}'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
body: {
|
||||
mode: 'grpc',
|
||||
grpc: [{
|
||||
name: messageName,
|
||||
content: messageContent
|
||||
}]
|
||||
grpc: [
|
||||
{
|
||||
name: messageName,
|
||||
content: messageContent
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
bodyws(_1, dictionary) {
|
||||
const pairs = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||
const namePair = _.find(pairs, { name: 'name' });
|
||||
const contentPair = _.find(pairs, { name: 'content' });
|
||||
|
||||
const messageName = namePair ? namePair.value : '';
|
||||
const messageContent = contentPair ? contentPair.value : '';
|
||||
|
||||
try {
|
||||
JSON.parse(messageContent);
|
||||
} catch (error) {
|
||||
console.error('Error validating ws message JSON:', error);
|
||||
return {
|
||||
body: {
|
||||
mode: 'ws',
|
||||
ws: [
|
||||
{
|
||||
name: messageName,
|
||||
content: '{}'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
body: {
|
||||
mode: 'ws',
|
||||
ws: [
|
||||
{
|
||||
name: messageName,
|
||||
content: messageContent
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -938,7 +985,7 @@ const parser = (input) => {
|
||||
const match = grammar.match(input);
|
||||
|
||||
if (match.succeeded()) {
|
||||
let ast = sem(match).ast
|
||||
let ast = sem(match).ast;
|
||||
|
||||
return ast;
|
||||
} else {
|
||||
@@ -946,4 +993,4 @@ const parser = (input) => {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = parser;
|
||||
module.exports = parser;
|
||||
|
||||
@@ -2,8 +2,8 @@ const _ = require('lodash');
|
||||
|
||||
const { indentString } = require('./utils');
|
||||
|
||||
const enabled = (items = [], key = "enabled") => items.filter((item) => item[key]);
|
||||
const disabled = (items = [], key = "enabled") => items.filter((item) => !item[key]);
|
||||
const enabled = (items = [], key = 'enabled') => items.filter((item) => item[key]);
|
||||
const disabled = (items = [], key = 'enabled') => items.filter((item) => !item[key]);
|
||||
|
||||
// remove the last line if two new lines are found
|
||||
const stripLastLine = (text) => {
|
||||
@@ -30,8 +30,23 @@ const getValueString = (value) => {
|
||||
};
|
||||
|
||||
const jsonToBru = (json) => {
|
||||
const { meta, http, grpc, params, headers, metadata, auth, body, script, tests, vars, assertions, settings, docs } = json;
|
||||
|
||||
const {
|
||||
meta,
|
||||
http,
|
||||
grpc,
|
||||
ws,
|
||||
params,
|
||||
headers,
|
||||
metadata,
|
||||
auth,
|
||||
body,
|
||||
script,
|
||||
tests,
|
||||
vars,
|
||||
assertions,
|
||||
settings,
|
||||
docs
|
||||
} = json;
|
||||
|
||||
let bru = '';
|
||||
|
||||
@@ -76,21 +91,21 @@ const jsonToBru = (json) => {
|
||||
`;
|
||||
}
|
||||
|
||||
if(grpc && grpc.url) {
|
||||
bru += `grpc {
|
||||
if (grpc && grpc.url) {
|
||||
bru += `grpc {
|
||||
url: ${grpc.url}`;
|
||||
|
||||
if(grpc.method && grpc.method.length) {
|
||||
if (grpc.method && grpc.method.length) {
|
||||
bru += `
|
||||
method: ${grpc.method}`;
|
||||
}
|
||||
|
||||
if(grpc.body && grpc.body.length) {
|
||||
if (grpc.body && grpc.body.length) {
|
||||
bru += `
|
||||
body: ${grpc.body}`;
|
||||
}
|
||||
|
||||
if(grpc.protoPath && grpc.protoPath.length) {
|
||||
if (grpc.protoPath && grpc.protoPath.length) {
|
||||
bru += `
|
||||
protoPath: ${grpc.protoPath}`;
|
||||
}
|
||||
@@ -111,6 +126,35 @@ const jsonToBru = (json) => {
|
||||
`;
|
||||
}
|
||||
|
||||
if (ws && ws.url) {
|
||||
bru += `ws {
|
||||
url: ${ws.url}`;
|
||||
|
||||
if (ws.method && ws.method.length) {
|
||||
bru += `
|
||||
method: ${ws.method}`;
|
||||
}
|
||||
|
||||
if (ws.body && ws.body.length) {
|
||||
bru += `
|
||||
body: ${ws.body}`;
|
||||
}
|
||||
|
||||
if (ws.auth && ws.auth.length) {
|
||||
bru += `
|
||||
auth: ${ws.auth}`;
|
||||
}
|
||||
|
||||
if (ws.methodType && ws.methodType.length) {
|
||||
bru += `
|
||||
methodType: ${ws.methodType}`;
|
||||
}
|
||||
|
||||
bru += `
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
if (params && params.length) {
|
||||
const queryParams = params.filter((param) => param.type === 'query');
|
||||
@@ -236,7 +280,6 @@ ${indentString(`password: ${auth?.digest?.password || ''}`)}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
if (auth && auth.ntlm) {
|
||||
bru += `auth:ntlm {
|
||||
${indentString(`username: ${auth?.ntlm?.username || ''}`)}
|
||||
@@ -246,7 +289,7 @@ ${indentString(`domain: ${auth?.ntlm?.domain || ''}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
if (auth && auth.oauth2) {
|
||||
switch (auth?.oauth2?.grantType) {
|
||||
@@ -263,10 +306,14 @@ ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
|
||||
${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}
|
||||
${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}
|
||||
${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
|
||||
auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
|
||||
}
|
||||
auth?.oauth2?.tokenPlacement == 'header'
|
||||
? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`)
|
||||
: ''
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header'
|
||||
? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`)
|
||||
: ''
|
||||
}
|
||||
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}
|
||||
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)}
|
||||
}
|
||||
@@ -288,10 +335,14 @@ ${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)}
|
||||
${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}
|
||||
${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}
|
||||
${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
|
||||
auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
|
||||
}
|
||||
auth?.oauth2?.tokenPlacement == 'header'
|
||||
? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`)
|
||||
: ''
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header'
|
||||
? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`)
|
||||
: ''
|
||||
}
|
||||
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}
|
||||
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)}
|
||||
}
|
||||
@@ -309,10 +360,14 @@ ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
|
||||
${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}
|
||||
${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}
|
||||
${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
|
||||
auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
|
||||
}
|
||||
auth?.oauth2?.tokenPlacement == 'header'
|
||||
? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`)
|
||||
: ''
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header'
|
||||
? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`)
|
||||
: ''
|
||||
}
|
||||
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}
|
||||
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)}
|
||||
}
|
||||
@@ -329,10 +384,14 @@ ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
|
||||
${indentString(`state: ${auth?.oauth2?.state || ''}`)}
|
||||
${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}
|
||||
${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
|
||||
auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
|
||||
}
|
||||
auth?.oauth2?.tokenPlacement == 'header'
|
||||
? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`)
|
||||
: ''
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header'
|
||||
? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`)
|
||||
: ''
|
||||
}
|
||||
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}
|
||||
}
|
||||
|
||||
@@ -341,107 +400,111 @@ ${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toStr
|
||||
}
|
||||
|
||||
if (auth?.oauth2?.additionalParameters) {
|
||||
const { authorization: authorizationParams, token: tokenParams, refresh: refreshParams } = auth?.oauth2?.additionalParameters;
|
||||
const authorizationHeaders = authorizationParams?.filter(p => p?.sendIn == 'headers');
|
||||
const {
|
||||
authorization: authorizationParams,
|
||||
token: tokenParams,
|
||||
refresh: refreshParams
|
||||
} = auth?.oauth2?.additionalParameters;
|
||||
const authorizationHeaders = authorizationParams?.filter((p) => p?.sendIn == 'headers');
|
||||
if (authorizationHeaders?.length) {
|
||||
bru += `auth:oauth2:additional_params:auth_req:headers {
|
||||
${indentString(
|
||||
authorizationHeaders
|
||||
.filter(item => item?.name?.length)
|
||||
.filter((item) => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}
|
||||
)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
const authorizationQueryParams = authorizationParams?.filter(p => p?.sendIn == 'queryparams');
|
||||
const authorizationQueryParams = authorizationParams?.filter((p) => p?.sendIn == 'queryparams');
|
||||
if (authorizationQueryParams?.length) {
|
||||
bru += `auth:oauth2:additional_params:auth_req:queryparams {
|
||||
${indentString(
|
||||
authorizationQueryParams
|
||||
.filter(item => item?.name?.length)
|
||||
.filter((item) => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}
|
||||
)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
const tokenHeaders = tokenParams?.filter(p => p?.sendIn == 'headers');
|
||||
const tokenHeaders = tokenParams?.filter((p) => p?.sendIn == 'headers');
|
||||
if (tokenHeaders?.length) {
|
||||
bru += `auth:oauth2:additional_params:access_token_req:headers {
|
||||
${indentString(
|
||||
tokenHeaders
|
||||
.filter(item => item?.name?.length)
|
||||
.filter((item) => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}
|
||||
)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
const tokenQueryParams = tokenParams?.filter(p => p?.sendIn == 'queryparams');
|
||||
const tokenQueryParams = tokenParams?.filter((p) => p?.sendIn == 'queryparams');
|
||||
if (tokenQueryParams?.length) {
|
||||
bru += `auth:oauth2:additional_params:access_token_req:queryparams {
|
||||
${indentString(
|
||||
tokenQueryParams
|
||||
.filter(item => item?.name?.length)
|
||||
.filter((item) => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}
|
||||
)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
const tokenBodyValues = tokenParams?.filter(p => p?.sendIn == 'body');
|
||||
const tokenBodyValues = tokenParams?.filter((p) => p?.sendIn == 'body');
|
||||
if (tokenBodyValues?.length) {
|
||||
bru += `auth:oauth2:additional_params:access_token_req:body {
|
||||
${indentString(
|
||||
tokenBodyValues
|
||||
.filter(item => item?.name?.length)
|
||||
.filter((item) => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}
|
||||
)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
const refreshHeaders = refreshParams?.filter(p => p?.sendIn == 'headers');
|
||||
const refreshHeaders = refreshParams?.filter((p) => p?.sendIn == 'headers');
|
||||
if (refreshHeaders?.length) {
|
||||
bru += `auth:oauth2:additional_params:refresh_token_req:headers {
|
||||
${indentString(
|
||||
refreshHeaders
|
||||
.filter(item => item?.name?.length)
|
||||
.filter((item) => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}
|
||||
)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
const refreshQueryParams = refreshParams?.filter(p => p?.sendIn == 'queryparams');
|
||||
const refreshQueryParams = refreshParams?.filter((p) => p?.sendIn == 'queryparams');
|
||||
if (refreshQueryParams?.length) {
|
||||
bru += `auth:oauth2:additional_params:refresh_token_req:queryparams {
|
||||
${indentString(
|
||||
refreshQueryParams
|
||||
.filter(item => item?.name?.length)
|
||||
.filter((item) => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}
|
||||
)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
const refreshBodyValues = refreshParams?.filter(p => p?.sendIn == 'body');
|
||||
const refreshBodyValues = refreshParams?.filter((p) => p?.sendIn == 'body');
|
||||
if (refreshBodyValues?.length) {
|
||||
bru += `auth:oauth2:additional_params:refresh_token_req:body {
|
||||
${indentString(
|
||||
refreshBodyValues
|
||||
.filter(item => item?.name?.length)
|
||||
.filter((item) => item?.name?.length)
|
||||
.map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}
|
||||
)}
|
||||
}
|
||||
|
||||
`;
|
||||
@@ -542,10 +605,9 @@ ${indentString(body.sparql)}
|
||||
bru += '\n}\n\n';
|
||||
}
|
||||
|
||||
|
||||
if (body && body.file && body.file.length) {
|
||||
bru += `body:file {`;
|
||||
const files = enabled(body.file, "selected").concat(disabled(body.file, "selected"));
|
||||
const files = enabled(body.file, 'selected').concat(disabled(body.file, 'selected'));
|
||||
|
||||
if (files.length) {
|
||||
bru += `\n${indentString(
|
||||
@@ -556,7 +618,7 @@ ${indentString(body.sparql)}
|
||||
item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : '';
|
||||
const filePath = item.filePath || '';
|
||||
const value = `@file(${filePath})`;
|
||||
const itemName = "file";
|
||||
const itemName = 'file';
|
||||
return `${selected}${itemName}: ${value}${contentType}`;
|
||||
})
|
||||
.join('\n')
|
||||
@@ -582,15 +644,35 @@ ${indentString(body.sparql)}
|
||||
// Convert each gRPC message to a separate body:grpc block
|
||||
if (Array.isArray(body.grpc)) {
|
||||
body.grpc.forEach((m) => {
|
||||
const {name, content} = m;
|
||||
|
||||
const { name, content } = m;
|
||||
|
||||
bru += `body:grpc {\n`;
|
||||
|
||||
|
||||
bru += `${indentString(`name: ${getValueString(name)}`)}\n`;
|
||||
|
||||
|
||||
// Convert content to JSON string if it's an object
|
||||
let jsonValue = typeof content === 'object' ? JSON.stringify(content, null, 2) : content || '{}';
|
||||
|
||||
|
||||
// Wrap content with triple quotes for multiline support, without extra indentation
|
||||
bru += `${indentString(`content: '''\n${indentString(jsonValue)}\n'''`)}\n`;
|
||||
bru += '}\n\n';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (body && body.ws) {
|
||||
// Convert each ws message to a separate body:ws block
|
||||
if (Array.isArray(body.ws)) {
|
||||
body.ws.forEach((m) => {
|
||||
const { name, content } = m;
|
||||
|
||||
bru += `body:ws {\n`;
|
||||
|
||||
bru += `${indentString(`name: ${getValueString(name)}`)}\n`;
|
||||
|
||||
// Convert content to JSON string if it's an object
|
||||
let jsonValue = typeof content === 'object' ? JSON.stringify(content, null, 2) : content || '{}';
|
||||
|
||||
// Wrap content with triple quotes for multiline support, without extra indentation
|
||||
bru += `${indentString(`content: '''\n${indentString(jsonValue)}\n'''`)}\n`;
|
||||
bru += '}\n\n';
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"axios": "^1.9.0",
|
||||
"grpc-reflection-js": "^0.3.0",
|
||||
"is-ip": "^5.0.1",
|
||||
"tough-cookie": "^6.0.0"
|
||||
"tough-cookie": "^6.0.0",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.22.0",
|
||||
|
||||
@@ -7,7 +7,6 @@ const peerDepsExternal = require('rollup-plugin-peer-deps-external');
|
||||
const json = require('@rollup/plugin-json');
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
@@ -29,16 +28,16 @@ module.exports = [
|
||||
peerDepsExternal(),
|
||||
nodeResolve({
|
||||
extensions: ['.js', '.ts', '.tsx', '.json', '.css'],
|
||||
dedupe: ['@grpc/grpc-js'],
|
||||
preferBuiltins: true
|
||||
dedupe: ['@grpc/grpc-js', 'ws'],
|
||||
preferBuiltins: true
|
||||
}),
|
||||
json(),
|
||||
commonjs({
|
||||
transformMixedEsModules: true
|
||||
}),
|
||||
typescript({ tsconfig: './tsconfig.json' }),
|
||||
terser(),
|
||||
terser()
|
||||
],
|
||||
external: ['axios', 'qs']
|
||||
external: ['axios', 'qs', 'ws']
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export { addDigestInterceptor, getOAuth2Token } from './auth';
|
||||
export { addDigestInterceptor, getOAuth2Token } from './auth';
|
||||
export { GrpcClient, generateGrpcSampleMessage } from './grpc';
|
||||
export { WsClient } from './ws/ws-client';
|
||||
export { default as cookies } from './cookies';
|
||||
|
||||
export * as network from './network';
|
||||
|
||||
export * as scripting from './scripting';
|
||||
export * as scripting from './scripting';
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { makeAxiosInstance } from './axios-instance';
|
||||
export { makeAxiosInstance } from './axios-instance';
|
||||
export { sendWsRequest } from './ws-request';
|
||||
|
||||
65
packages/bruno-requests/src/network/ws-request.ts
Normal file
65
packages/bruno-requests/src/network/ws-request.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { WsClient } from '../ws/ws-client';
|
||||
|
||||
export const sendWsRequest = async (item: any, collection: any, environment: any, runtimeVariables: any) => {
|
||||
// For CLI, we'll create a simple WebSocket client that connects, sends messages, and closes
|
||||
// This is a simplified version for CLI usage
|
||||
const request = item.draft ? item.draft : item;
|
||||
const url = request.request.url;
|
||||
const headers = request.request.headers || [];
|
||||
const wsMessages = request.request.body?.ws || [];
|
||||
|
||||
if (!url) {
|
||||
throw new Error('WebSocket URL is required');
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// For CLI, we'll simulate the WebSocket connection
|
||||
// In a real implementation, this would use the ws library
|
||||
console.log(`Connecting to WebSocket: ${url}`);
|
||||
|
||||
// Simulate connection time
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// Return a response structure similar to HTTP responses
|
||||
return {
|
||||
status: 'CONNECTED',
|
||||
statusCode: 0,
|
||||
statusText: 'CONNECTED',
|
||||
headers: headers,
|
||||
body: '',
|
||||
size: 0,
|
||||
duration: duration,
|
||||
responses: wsMessages.map((msg: any, index: number) => ({
|
||||
message: msg.content || '{}',
|
||||
direction: 'sent',
|
||||
timestamp: startTime + index * 100
|
||||
})),
|
||||
isError: false,
|
||||
error: null,
|
||||
errorDetails: null,
|
||||
metadata: [],
|
||||
trailers: []
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
status: 'ERROR',
|
||||
statusCode: 0,
|
||||
statusText: 'ERROR',
|
||||
headers: [],
|
||||
body: '',
|
||||
size: 0,
|
||||
duration: duration,
|
||||
responses: [],
|
||||
isError: true,
|
||||
error: error instanceof Error ? error.message : 'WebSocket connection failed',
|
||||
errorDetails: error instanceof Error ? error.stack : null,
|
||||
metadata: [],
|
||||
trailers: []
|
||||
};
|
||||
}
|
||||
};
|
||||
320
packages/bruno-requests/src/ws/ws-client.js
Normal file
320
packages/bruno-requests/src/ws/ws-client.js
Normal file
@@ -0,0 +1,320 @@
|
||||
import ws from 'ws';
|
||||
|
||||
/**
|
||||
* Safely parse JSON string with error handling
|
||||
* @param {string} jsonString - The JSON string to parse
|
||||
* @param {string} context - Context for error messages
|
||||
* @returns {Object} Parsed object or throws error with context
|
||||
* @throws {Error} If JSON parsing fails
|
||||
*/
|
||||
const safeJsonParse = (jsonString, context = 'JSON string') => {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
const errorMessage = `Failed to parse ${context}: ${error.message}`;
|
||||
console.error(errorMessage, {
|
||||
originalString: jsonString,
|
||||
parseError: error
|
||||
});
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Process WebSocket headers into the format expected by the ws library
|
||||
* @param {Array} headers - Array of header objects with name and value
|
||||
* @returns {Object} Headers object for WebSocket connection
|
||||
*/
|
||||
const processWsHeaders = (headers) => {
|
||||
const processedHeaders = {};
|
||||
|
||||
if (Array.isArray(headers)) {
|
||||
headers.forEach((header) => {
|
||||
if (header.name && header.value !== undefined) {
|
||||
processedHeaders[header.name] = header.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return processedHeaders;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get parsed WebSocket URL object
|
||||
* @param {string} url - The WebSocket URL
|
||||
* @returns {Object} Parsed URL object with protocol, host, path
|
||||
*/
|
||||
const getParsedWsUrlObject = (url) => {
|
||||
const addProtocolIfMissing = (str) => {
|
||||
if (str.includes('://')) return str;
|
||||
|
||||
// For localhost, default to insecure (grpc://) for local development
|
||||
if (str.includes('localhost') || str.includes('127.0.0.1')) {
|
||||
return `ws://${str}`;
|
||||
}
|
||||
|
||||
// For other hosts, default to secure
|
||||
return `wss://${str}`;
|
||||
};
|
||||
|
||||
const removeTrailingSlash = (str) => (str.endsWith('/') ? str.slice(0, -1) : str);
|
||||
|
||||
if (!url) return { host: '', path: '' };
|
||||
|
||||
try {
|
||||
const urlObj = new URL(addProtocolIfMissing(url.toLowerCase()));
|
||||
return {
|
||||
protocol: urlObj.protocol,
|
||||
host: urlObj.host,
|
||||
path: removeTrailingSlash(urlObj.pathname),
|
||||
search: urlObj.search,
|
||||
fullUrl: urlObj.href
|
||||
};
|
||||
} catch (err) {
|
||||
console.error({ err });
|
||||
return {
|
||||
host: '',
|
||||
path: ''
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
class WsClient {
|
||||
constructor(eventCallback) {
|
||||
this.activeConnections = new Map();
|
||||
this.eventCallback = eventCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a WebSocket connection
|
||||
* @param {Object} params - Connection parameters
|
||||
* @param {Object} params.request - The WebSocket request object
|
||||
* @param {Object} params.collection - The collection object
|
||||
* @param {Object} params.options - Additional connection options
|
||||
*/
|
||||
async startConnection({ request, collection, options = {} }) {
|
||||
const { url, headers = [] } = request;
|
||||
const { timeout = 30000, keepAlive = true } = options;
|
||||
|
||||
const parsedUrl = getParsedWsUrlObject(url);
|
||||
const processedHeaders = processWsHeaders(headers);
|
||||
|
||||
const requestId = request.uid;
|
||||
const collectionUid = collection.uid;
|
||||
|
||||
try {
|
||||
// Create WebSocket connection
|
||||
const wsConnection = new ws.WebSocket(parsedUrl.fullUrl, {
|
||||
headers: processedHeaders,
|
||||
timeout: timeout,
|
||||
followRedirects: true
|
||||
});
|
||||
|
||||
// Set up event handlers
|
||||
this.#setupWsEventHandlers(wsConnection, requestId, collectionUid);
|
||||
|
||||
// Store the connection
|
||||
this.#addConnection(requestId, wsConnection);
|
||||
|
||||
// Emit connecting event
|
||||
this.eventCallback('ws:connecting', requestId, collectionUid, {
|
||||
url: parsedUrl.fullUrl,
|
||||
headers: processedHeaders,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating WebSocket connection:', error);
|
||||
this.eventCallback('ws:error', requestId, collectionUid, {
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to an active WebSocket connection
|
||||
* @param {string} requestId - The request ID of the active connection
|
||||
* @param {string} collectionUid - The collection UID for the request
|
||||
* @param {Object|string} message - The message to send
|
||||
*/
|
||||
sendMessage(requestId, collectionUid, message) {
|
||||
const connection = this.activeConnections.get(requestId);
|
||||
|
||||
if (connection && connection.readyState === WebSocket.OPEN) {
|
||||
let messageToSend;
|
||||
|
||||
// Parse the message if it's a string
|
||||
if (typeof message === 'string') {
|
||||
try {
|
||||
messageToSend = safeJsonParse(message, 'message content');
|
||||
} catch (parseError) {
|
||||
// If parsing fails, send as string
|
||||
messageToSend = message;
|
||||
}
|
||||
} else {
|
||||
messageToSend = message;
|
||||
}
|
||||
|
||||
// Send the message
|
||||
connection.send(JSON.stringify(messageToSend), (error) => {
|
||||
if (error) {
|
||||
this.eventCallback('ws:error', requestId, collectionUid, { error });
|
||||
} else {
|
||||
// Emit message sent event
|
||||
this.eventCallback('ws:message', requestId, collectionUid, {
|
||||
message: messageToSend,
|
||||
direction: 'outgoing',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const error = new Error('WebSocket connection not available or not open');
|
||||
this.eventCallback('ws:error', requestId, collectionUid, {
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a WebSocket connection
|
||||
* @param {string} requestId - The request ID to close
|
||||
* @param {number} code - Close code (optional)
|
||||
* @param {string} reason - Close reason (optional)
|
||||
*/
|
||||
close(requestId, code = 1000, reason = 'Client initiated close') {
|
||||
const connection = this.activeConnections.get(requestId);
|
||||
|
||||
if (connection) {
|
||||
connection.close(code, reason);
|
||||
this.#removeConnection(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a connection is active
|
||||
* @param {string} requestId - The request ID to check
|
||||
* @returns {boolean} - Whether the connection is active
|
||||
*/
|
||||
isConnectionActive(requestId) {
|
||||
const connection = this.activeConnections.get(requestId);
|
||||
return connection && connection.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active connection IDs
|
||||
* @returns {string[]} Array of active connection IDs
|
||||
*/
|
||||
getActiveConnectionIds() {
|
||||
return Array.from(this.activeConnections.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all active connections
|
||||
*/
|
||||
clearAllConnections() {
|
||||
const connectionIds = this.getActiveConnectionIds();
|
||||
|
||||
this.activeConnections.forEach((connection) => {
|
||||
if (connection.readyState === WebSocket.OPEN) {
|
||||
connection.close(1000, 'Client clearing all connections');
|
||||
}
|
||||
});
|
||||
|
||||
this.activeConnections.clear();
|
||||
|
||||
// Emit an event with empty active connection IDs
|
||||
if (connectionIds.length > 0) {
|
||||
this.eventCallback('ws:connections-changed', {
|
||||
type: 'cleared',
|
||||
activeConnectionIds: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up WebSocket event handlers
|
||||
* @param {WebSocket} ws - The WebSocket instance
|
||||
* @param {string} requestId - The request ID
|
||||
* @param {string} collectionUid - The collection UID
|
||||
* @private
|
||||
*/
|
||||
#setupWsEventHandlers(ws, requestId, collectionUid) {
|
||||
ws.on('open', () => {
|
||||
this.eventCallback('ws:open', requestId, collectionUid, {
|
||||
timestamp: Date.now()
|
||||
});
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
try {
|
||||
const message = JSON.parse(data.toString());
|
||||
this.eventCallback('ws:message', requestId, collectionUid, {
|
||||
message,
|
||||
direction: 'incoming',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
} catch (error) {
|
||||
// If parsing fails, send as raw data
|
||||
this.eventCallback('ws:message', requestId, collectionUid, {
|
||||
message: data.toString(),
|
||||
direction: 'incoming',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', (code, reason) => {
|
||||
this.eventCallback('ws:close', requestId, collectionUid, {
|
||||
code,
|
||||
reason: reason.toString(),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
this.#removeConnection(requestId);
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
this.eventCallback('ws:error', requestId, collectionUid, {
|
||||
error: error.message,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a connection to the active connections map and emit an event
|
||||
* @param {string} requestId - The request ID
|
||||
* @param {WebSocket} connection - The WebSocket connection
|
||||
* @private
|
||||
*/
|
||||
#addConnection(requestId, connection) {
|
||||
this.activeConnections.set(requestId, connection);
|
||||
|
||||
// Emit an event with all active connection IDs
|
||||
this.eventCallback('ws:connections-changed', {
|
||||
type: 'added',
|
||||
requestId,
|
||||
activeConnectionIds: this.getActiveConnectionIds()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a connection from the active connections map and emit an event
|
||||
* @param {string} requestId - The request ID
|
||||
* @private
|
||||
*/
|
||||
#removeConnection(requestId) {
|
||||
if (this.activeConnections.has(requestId)) {
|
||||
this.activeConnections.delete(requestId);
|
||||
|
||||
// Emit an event with all active connection IDs
|
||||
this.eventCallback('ws:connections-changed', {
|
||||
type: 'removed',
|
||||
requestId,
|
||||
activeConnectionIds: this.getActiveConnectionIds()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { WsClient };
|
||||
@@ -74,8 +74,7 @@ const multipartFormSchema = Yup.object({
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
|
||||
const fileSchema = Yup.object({
|
||||
const fileSchema = Yup.object({
|
||||
uid: uidSchema,
|
||||
filePath: Yup.string().nullable(),
|
||||
contentType: Yup.string().nullable(),
|
||||
@@ -138,16 +137,13 @@ const authDigestSchema = Yup.object({
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
|
||||
|
||||
const authNTLMSchema = Yup.object({
|
||||
username: Yup.string().nullable(),
|
||||
password: Yup.string().nullable(),
|
||||
domain: Yup.string().nullable()
|
||||
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
const authNTLMSchema = Yup.object({
|
||||
username: Yup.string().nullable(),
|
||||
password: Yup.string().nullable(),
|
||||
domain: Yup.string().nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const authApiKeySchema = Yup.object({
|
||||
key: Yup.string().nullable(),
|
||||
@@ -160,24 +156,20 @@ const authApiKeySchema = Yup.object({
|
||||
const oauth2AuthorizationAdditionalParametersSchema = Yup.object({
|
||||
name: Yup.string().nullable(),
|
||||
value: Yup.string().nullable(),
|
||||
sendIn: Yup.string()
|
||||
.oneOf(['headers', 'queryparams'])
|
||||
.required('send in property is required'),
|
||||
sendIn: Yup.string().oneOf(['headers', 'queryparams']).required('send in property is required'),
|
||||
enabled: Yup.boolean()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const oauth2AdditionalParametersSchema = Yup.object({
|
||||
name: Yup.string().nullable(),
|
||||
value: Yup.string().nullable(),
|
||||
sendIn: Yup.string()
|
||||
.oneOf(['headers', 'queryparams', 'body'])
|
||||
.required('send in property is required'),
|
||||
enabled: Yup.boolean()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
name: Yup.string().nullable(),
|
||||
value: Yup.string().nullable(),
|
||||
sendIn: Yup.string().oneOf(['headers', 'queryparams', 'body']).required('send in property is required'),
|
||||
enabled: Yup.boolean()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const oauth2Schema = Yup.object({
|
||||
grantType: Yup.string()
|
||||
@@ -249,14 +241,16 @@ const oauth2Schema = Yup.object({
|
||||
otherwise: Yup.string().nullable().strip()
|
||||
}),
|
||||
tokenHeaderPrefix: Yup.string().when(['grantType', 'tokenPlacement'], {
|
||||
is: (grantType, tokenPlacement) =>
|
||||
['client_credentials', 'password', 'authorization_code', 'implicit'].includes(grantType) && tokenPlacement === 'header',
|
||||
is: (grantType, tokenPlacement) =>
|
||||
['client_credentials', 'password', 'authorization_code', 'implicit'].includes(grantType) &&
|
||||
tokenPlacement === 'header',
|
||||
then: Yup.string().nullable(),
|
||||
otherwise: Yup.string().nullable().strip()
|
||||
}),
|
||||
tokenQueryKey: Yup.string().when(['grantType', 'tokenPlacement'], {
|
||||
is: (grantType, tokenPlacement) =>
|
||||
['client_credentials', 'password', 'authorization_code', 'implicit'].includes(grantType) && tokenPlacement === 'url',
|
||||
is: (grantType, tokenPlacement) =>
|
||||
['client_credentials', 'password', 'authorization_code', 'implicit'].includes(grantType) &&
|
||||
tokenPlacement === 'url',
|
||||
then: Yup.string().nullable(),
|
||||
otherwise: Yup.string().nullable().strip()
|
||||
}),
|
||||
@@ -355,10 +349,51 @@ const grpcRequestSchema = Yup.object({
|
||||
auth: authSchema,
|
||||
body: Yup.object({
|
||||
mode: Yup.string().oneOf(['grpc']).required('mode is required'),
|
||||
grpc: Yup.array().of(Yup.object({
|
||||
name: Yup.string().nullable(),
|
||||
content: Yup.string().nullable()
|
||||
})).nullable()
|
||||
grpc: Yup.array()
|
||||
.of(
|
||||
Yup.object({
|
||||
name: Yup.string().nullable(),
|
||||
content: Yup.string().nullable()
|
||||
})
|
||||
)
|
||||
.nullable()
|
||||
})
|
||||
.strict()
|
||||
.required('body is required'),
|
||||
script: Yup.object({
|
||||
req: Yup.string().nullable(),
|
||||
res: Yup.string().nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict(),
|
||||
vars: Yup.object({
|
||||
req: Yup.array().of(varsSchema).nullable(),
|
||||
res: Yup.array().of(varsSchema).nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict()
|
||||
.nullable(),
|
||||
assertions: Yup.array().of(keyValueSchema).nullable(),
|
||||
tests: Yup.string().nullable(),
|
||||
docs: Yup.string().nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const wsRequestSchema = Yup.object({
|
||||
url: requestUrlSchema,
|
||||
headers: Yup.array().of(keyValueSchema).required('headers are required'),
|
||||
auth: authSchema,
|
||||
body: Yup.object({
|
||||
mode: Yup.string().oneOf(['ws']).required('mode is required'),
|
||||
ws: Yup.array()
|
||||
.of(
|
||||
Yup.object({
|
||||
name: Yup.string().nullable(),
|
||||
content: Yup.string().nullable()
|
||||
})
|
||||
)
|
||||
.nullable()
|
||||
})
|
||||
.strict()
|
||||
.required('body is required'),
|
||||
@@ -419,16 +454,22 @@ const folderRootSchema = Yup.object({
|
||||
|
||||
const itemSchema = Yup.object({
|
||||
uid: uidSchema,
|
||||
type: Yup.string().oneOf(['http-request', 'graphql-request', 'folder', 'js', 'grpc-request']).required('type is required'),
|
||||
type: Yup.string()
|
||||
.oneOf(['http-request', 'graphql-request', 'folder', 'js', 'grpc-request', 'ws-request'])
|
||||
.required('type is required'),
|
||||
seq: Yup.number().min(1),
|
||||
name: Yup.string().min(1, 'name must be at least 1 character').required('name is required'),
|
||||
tags: Yup.array().of(Yup.string().matches(/^[\w-]+$/, 'tag must be alphanumeric')),
|
||||
request: Yup.mixed().when('type', {
|
||||
is: (type) => type === 'grpc-request',
|
||||
then: grpcRequestSchema.required('request is required when item-type is grpc-request'),
|
||||
otherwise: requestSchema.when('type', {
|
||||
is: (type) => ['http-request', 'graphql-request'].includes(type),
|
||||
then: (schema) => schema.required('request is required when item-type is request')
|
||||
otherwise: Yup.mixed().when('type', {
|
||||
is: (type) => type === 'ws-request',
|
||||
then: wsRequestSchema.required('request is required when item-type is ws-request'),
|
||||
otherwise: requestSchema.when('type', {
|
||||
is: (type) => ['http-request', 'graphql-request'].includes(type),
|
||||
then: (schema) => schema.required('request is required when item-type is request')
|
||||
})
|
||||
})
|
||||
}),
|
||||
settings: Yup.object({
|
||||
|
||||
Reference in New Issue
Block a user