mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 09:28:33 +00:00
Compare commits
5 Commits
bugfix/inc
...
v2.12.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
068900866c | ||
|
|
fa5ac0d460 | ||
|
|
c8da13bd9b | ||
|
|
86727c8525 | ||
|
|
901b6daaea |
2
.github/workflows/npm-bru-cli.yml
vendored
2
.github/workflows/npm-bru-cli.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
6
.github/workflows/ssl-tests.yml
vendored
6
.github/workflows/ssl-tests.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
|
||||
8
.github/workflows/tests.yml
vendored
8
.github/workflows/tests.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -34,8 +34,6 @@ jobs:
|
||||
|
||||
- name: Lint Check
|
||||
run: npm run lint
|
||||
env:
|
||||
ESLINT_PLUGIN_DIFF_COMMIT: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || 'main' }}
|
||||
|
||||
# tests
|
||||
- name: Test Package bruno-js
|
||||
@@ -66,7 +64,7 @@ jobs:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -108,7 +106,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: v22.11.x
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
npx nano-staged
|
||||
@@ -37,37 +37,13 @@ Bruno 直接在您的电脑文件夹中存储您的 API 信息。我们使用纯
|
||||
|
||||
Bruno 仅限离线使用。我们计划永不向 Bruno 添加云同步功能。我们重视您的数据隐私,并认为它应该留在您的设备上。阅读我们的长期愿景 [点击查看](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
[下载 Bruno](https://www.usebruno.com/downloads)
|
||||
|
||||
📢 观看我们在印度 FOSS 3.0 会议上的最新演讲 [点击查看](https://www.youtube.com/watch?v=7bSMFpbcPiY)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
## 商业版本 ✨
|
||||
### 安装
|
||||
|
||||
我们的大多数功能都是免费且开源的。
|
||||
我们致力于在 [开源与可持续性发展](https://github.com/usebruno/bruno/discussions/269) 之间取得和谐的平衡
|
||||
|
||||
欢迎使用我们的 [付费版本](https://www.usebruno.com/pricing) ,看看附加的功能是否对您或团队有所帮助! <br/>
|
||||
|
||||
## 目录
|
||||
- [安装](#安装)
|
||||
- [特性](#特性)
|
||||
- [跨平台使用 🖥️](#跨平台使用-)
|
||||
- [通过Git协作 👩💻🧑💻](#通过git协作-)
|
||||
- [重要链接 📌](#重要链接-)
|
||||
- [展示 🎥](#展示-)
|
||||
- [分享评价 📣](#分享评价-)
|
||||
- [发布到新的包管理器](#发布到新的包管理器)
|
||||
- [联系方式 🌐](#联系方式-)
|
||||
- [商标](#商标)
|
||||
- [贡献 👩💻🧑💻](#贡献-)
|
||||
- [作者](#作者)
|
||||
- [许可证 📄](#许可证-)
|
||||
|
||||
## 安装
|
||||
|
||||
Bruno 可以在我们的 [网站上下载](https://www.usebruno.com/downloads) 适用于Mac、Windows 和 Linux 的可执行文件。
|
||||
Bruno 可以在我们的 [网站上下载](https://www.usebruno.com/downloads) Mac、Windows 和 Linux 的可执行文件。
|
||||
|
||||
您也可以通过包管理器如 Homebrew、Chocolatey、Scoop、Snap 和 Apt 安装 Bruno。
|
||||
|
||||
@@ -82,15 +58,9 @@ choco install bruno
|
||||
scoop bucket add extras
|
||||
scoop install bruno
|
||||
|
||||
# 在 Windows 上用 winget 安装
|
||||
winget install Bruno.Bruno
|
||||
|
||||
# 在 Linux 上用 Snap 安装
|
||||
snap install bruno
|
||||
|
||||
# 在 Linux 上用 Flatpak 安装
|
||||
flatpak install com.usebruno.Bruno
|
||||
|
||||
# 在 Linux 上用 Apt 安装
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
@@ -103,50 +73,67 @@ echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebr
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
## 特性
|
||||
|
||||
### 跨平台使用 🖥️
|
||||
### 在 Mac 上通过 Homebrew 安装 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### 通过Git协作 👩💻🧑💻
|
||||
### Collaborate 安装 👩💻🧑💻
|
||||
|
||||
或者任何您选择的版本控制系统
|
||||
|
||||
 <br /><br />
|
||||
|
||||
## 重要链接 📌
|
||||
### 重要链接 📌
|
||||
|
||||
- [我们的愿景](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [路线图](https://www.usebruno.com/roadmap)
|
||||
- [路线图](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [文档](https://docs.usebruno.com)
|
||||
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
|
||||
- [网站](https://www.usebruno.com)
|
||||
- [价格](https://www.usebruno.com/pricing)
|
||||
- [下载](https://www.usebruno.com/downloads)
|
||||
- [GitHub 赞助](https://github.com/sponsors/helloanoop).
|
||||
|
||||
## 展示 🎥
|
||||
### 展示 🎥
|
||||
|
||||
- [Testimonials](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
## 分享评价 📣
|
||||
### 支持 ❤️
|
||||
|
||||
如果您喜欢 Bruno 并想支持我们的开源工作,请考虑通过 [GitHub Sponsors](https://github.com/sponsors/helloanoop) 来赞助我们。
|
||||
|
||||
### 分享评价 📣
|
||||
|
||||
如果 Bruno 在您的工作和团队中帮助了您,请不要忘记在我们的 GitHub 讨论上分享您的 [评价](https://github.com/usebruno/bruno/discussions/343)
|
||||
|
||||
## 发布到新的包管理器
|
||||
### 发布到新的包管理器
|
||||
|
||||
如需了解更多信息,请参见 [此处](../publishing/publishing_cn.md) 。
|
||||
有关更多信息,请参见 [此处](../publishing/publishing_cn.md) 。
|
||||
|
||||
## 联系方式 🌐
|
||||
### 贡献 👩💻🧑💻
|
||||
|
||||
我很高兴您希望改进 bruno。请查看 [贡献指南](../contributing/contributing_cn.md)。
|
||||
|
||||
即使您无法通过代码做出贡献,我们仍然欢迎您提出 BUG 和新的功能需求。
|
||||
|
||||
### 作者
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### 联系方式 🌐
|
||||
|
||||
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
|
||||
[Website](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[LinkedIn](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
## 商标
|
||||
### 商标
|
||||
|
||||
**名称**
|
||||
|
||||
@@ -156,20 +143,6 @@ sudo apt update && sudo apt install bruno
|
||||
|
||||
Logo 源自 [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
## 贡献 👩💻🧑💻
|
||||
|
||||
很高兴您希望改进 bruno。请查看 [贡献指南](../contributing/contributing_cn.md)。
|
||||
|
||||
即使您无法通过代码做出贡献,我们仍然欢迎您提出 BUG 和新的功能需求。
|
||||
|
||||
## 作者
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## 许可证 📄
|
||||
### 许可证 📄
|
||||
|
||||
[MIT](../../license.md)
|
||||
|
||||
@@ -1,70 +1,8 @@
|
||||
// eslint.config.js
|
||||
const { defineConfig } = require("eslint/config");
|
||||
const globals = require("globals");
|
||||
const { fixupPluginRules } = require('@eslint/compat');
|
||||
const eslintPluginDiff = require('eslint-plugin-diff');
|
||||
|
||||
let stylistic;
|
||||
|
||||
const runESMImports = async () => {
|
||||
stylistic = await import('@stylistic/eslint-plugin').then(d => d.default);
|
||||
};
|
||||
|
||||
module.exports = runESMImports().then(() => defineConfig([
|
||||
{
|
||||
plugins: {
|
||||
'diff': fixupPluginRules(eslintPluginDiff),
|
||||
'@stylistic': stylistic,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: require('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
}
|
||||
},
|
||||
files: [
|
||||
'./eslint.config.js',
|
||||
'tests/**/*.{ts,js}',
|
||||
'packages/bruno-app/**/*.{js,jsx,ts}',
|
||||
'packages/bruno-app/src/test-utils/mocks/codemirror.js',
|
||||
'packages/bruno-cli/**/*.js',
|
||||
'packages/bruno-common/**/*.ts',
|
||||
'packages/bruno-converters/**/*.js',
|
||||
'packages/bruno-electron/**/*.js',
|
||||
'packages/bruno-filestore/**/*.ts',
|
||||
'packages/bruno-js/**/*.js',
|
||||
'packages/bruno-lang/**/*.js',
|
||||
'packages/bruno-requests/**/*.ts',
|
||||
'packages/bruno-requests/**/*.js',
|
||||
],
|
||||
processor: 'diff/diff',
|
||||
rules: {
|
||||
...stylistic.configs.customize({
|
||||
indent: 2,
|
||||
quotes: 'single',
|
||||
semi: true,
|
||||
jsx: true,
|
||||
}).rules,
|
||||
'@stylistic/comma-dangle': ['error', 'never'],
|
||||
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }],
|
||||
'@stylistic/arrow-parens': ['error', 'always'],
|
||||
'@stylistic/curly-newline': ['error', {
|
||||
multiline: true,
|
||||
minElements: 2,
|
||||
consistent: true,
|
||||
}],
|
||||
'@stylistic/function-paren-newline': ['error', 'never'],
|
||||
'@stylistic/array-bracket-spacing': ['error', 'never'],
|
||||
'@stylistic/arrow-spacing': ['error', { before: true, after: true }],
|
||||
'@stylistic/function-call-spacing': ['error', 'never'],
|
||||
'@stylistic/multiline-ternary': ['off'],
|
||||
'@stylistic/padding-line-between-statements': ['off'],
|
||||
'@stylistic/semi-style': ['error', 'last'],
|
||||
'@stylistic/max-len': ['off'],
|
||||
'@stylistic/jsx-one-expression-per-line': ['off']
|
||||
},
|
||||
},
|
||||
module.exports = defineConfig([
|
||||
{
|
||||
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],
|
||||
ignores: ["**/*.config.js", "**/public/**/*"],
|
||||
@@ -259,4 +197,4 @@ module.exports = runESMImports().then(() => defineConfig([
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
]));
|
||||
]);
|
||||
351
package-lock.json
generated
351
package-lock.json
generated
@@ -22,25 +22,20 @@
|
||||
"packages/bruno-filestore"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.3.2",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@stylistic/eslint-plugin": "^5.3.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.14.1",
|
||||
"@typescript-eslint/parser": "^8.39.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-plugin-diff": "^2.0.3",
|
||||
"fs-extra": "^11.1.1",
|
||||
"globals": "^16.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nano-staged": "^0.8.0",
|
||||
"playwright": "^1.51.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
@@ -3658,9 +3653,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/get/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"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": {
|
||||
@@ -3788,24 +3783,6 @@
|
||||
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/compat": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.2.tgz",
|
||||
"integrity": "sha512-jRNwzTbd6p2Rw4sZ1CgWRS8YMtqG15YyZf7zvb6gY2rB2u6n+2Z+ELW0GtL0fQgyl0pr4Y/BzBfng/BdsereRA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.40 || 9"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
|
||||
@@ -7834,64 +7811,6 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.3.1.tgz",
|
||||
"integrity": "sha512-Ykums1VYonM0TgkD0VteVq9mrlO2FhF48MDJnPyv3MktIB2ydtuhlO0AfWm7xnW1kyf5bjOqA6xc7JjviuVTxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/types": "^8.41.0",
|
||||
"eslint-visitor-keys": "^4.2.1",
|
||||
"espree": "^10.4.0",
|
||||
"estraverse": "^5.3.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.0.tgz",
|
||||
"integrity": "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/estraverse": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
|
||||
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
@@ -9221,9 +9140,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -10017,16 +9936,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bluebird": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||
@@ -12893,9 +12802,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "37.6.1",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-37.6.1.tgz",
|
||||
"integrity": "sha512-aHtJVNjqf0lk7dlPoc1X+fMBpZtLn+XGvP6IYc3gooTwsD1D/Ic2SBRC9SnIk6LkWTsDaSF9jgH1d9Q7eABy/Q==",
|
||||
"version": "37.2.6",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-37.2.6.tgz",
|
||||
"integrity": "sha512-Ns6xyxE+hIK5UlujtRlw7w4e2Ju/ImCWXf1Q/PoOhc0N3/6SN6YW7+ujCarsHbxWnolbW+1RlkHtdklUJpjbPA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -13355,19 +13264,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-diff": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-diff/-/eslint-plugin-diff-2.0.3.tgz",
|
||||
"integrity": "sha512-pkSwOTjPxqtplq+Ea+9i75QMdmJrItu6oxIG6EWY8bJ5veBNMUrp6Oti+5L6SW20FfjX+CmlZz+fyiWGG1POaQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=6.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||
@@ -13575,15 +13471,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
|
||||
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"acorn": "^8.15.0",
|
||||
"acorn": "^8.14.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
"eslint-visitor-keys": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -14169,9 +14065,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/extract-zip/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"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": {
|
||||
@@ -14387,13 +14283,6 @@
|
||||
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
@@ -15114,9 +15003,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/global-agent/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"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",
|
||||
"optional": true,
|
||||
@@ -15457,18 +15346,6 @@
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/hexy": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/hexy/-/hexy-0.3.5.tgz",
|
||||
"integrity": "sha512-UCP7TIZPXz5kxYJnNOym+9xaenxCLor/JyhKieo8y8/bJWunGh9xbhy3YrgYJUQ87WwfXGm05X330DszOfINZw==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"hexy": "bin/hexy_cmd.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hey-listen": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
|
||||
@@ -15712,9 +15589,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
@@ -15915,22 +15792,6 @@
|
||||
"node": ">=10.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/husky": {
|
||||
"version": "9.1.7",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
|
||||
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"husky": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "24.1.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.1.2.tgz",
|
||||
@@ -18682,28 +18543,6 @@
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/macos-export-certificate-and-key": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/macos-export-certificate-and-key/-/macos-export-certificate-and-key-1.2.4.tgz",
|
||||
"integrity": "sha512-y5QZEywlBNKd+EhPZ1Hz1FmDbbeQKtuVHJaTlawdl7vXw9bi/4tJB2xSMwX4sMVcddy3gbQ8K0IqXAi2TpDo2g==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"node-addon-api": "^4.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/macos-export-certificate-and-key/node_modules/node-addon-api": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
|
||||
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
|
||||
@@ -19383,22 +19222,6 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/nano-staged": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/nano-staged/-/nano-staged-0.8.0.tgz",
|
||||
"integrity": "sha512-QSEqPGTCJbkHU2yLvfY6huqYPjdBrOaTMKatO1F8nCSrkQGXeKwtCiCnsdxnuMhbg3DTVywKaeWLGCE5oJpq0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picocolors": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"nano-staged": "lib/bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoclone": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz",
|
||||
@@ -20297,18 +20120,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pidusage": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pidusage/-/pidusage-4.0.1.tgz",
|
||||
"integrity": "sha512-yCH2dtLHfEBnzlHUJymR/Z1nN2ePG3m392Mv8TFlTP1B0xkpMQNHAnfkY0n2tAi6ceKO6YWhxYfZ96V4vVkh/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/pify": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
|
||||
@@ -24881,9 +24692,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sumchecker/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"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": {
|
||||
@@ -25017,16 +24828,6 @@
|
||||
"jscat": "bundle.js"
|
||||
}
|
||||
},
|
||||
"node_modules/system-ca": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/system-ca/-/system-ca-2.0.1.tgz",
|
||||
"integrity": "sha512-9ZDV9yl8ph6Op67wDGPr4LykX86usE9x3le+XZSHfVMiiVJ5IRgmCWjLgxyz35ju9H3GDIJJZm4ogAeIfN5cQQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optionalDependencies": {
|
||||
"macos-export-certificate-and-key": "^1.2.0",
|
||||
"win-export-certificate-and-key": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.17",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||
@@ -26503,28 +26304,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/win-export-certificate-and-key": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/win-export-certificate-and-key/-/win-export-certificate-and-key-2.1.0.tgz",
|
||||
"integrity": "sha512-WeMLa/2uNZcS/HWGKU2G1Gzeh3vHpV/UFvwLhJLKxPHYFAbubxxVcJbqmPXaqySWK1Ymymh16zKK5WYIJ3zgzA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"node-addon-api": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/win-export-certificate-and-key/node_modules/node-addon-api": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
|
||||
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
@@ -30262,7 +30041,6 @@
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"graphql": "^16.6.0",
|
||||
"hexy": "^0.3.5",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
@@ -30271,7 +30049,6 @@
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"nanoid": "3.3.8",
|
||||
"pidusage": "^4.0.1",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^6.0.0",
|
||||
@@ -30279,7 +30056,7 @@
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "~37.6.1",
|
||||
"electron": "~37.2.6",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-devtools-installer": "^4.0.0"
|
||||
},
|
||||
@@ -31986,7 +31763,6 @@
|
||||
"cheerio": "^1.0.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"json-query": "^2.2.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"nanoid": "3.3.8",
|
||||
@@ -32073,12 +31849,9 @@
|
||||
"@grpc/proto-loader": "^0.7.15",
|
||||
"@types/qs": "^6.9.18",
|
||||
"axios": "^1.9.0",
|
||||
"debug": "^4.4.3",
|
||||
"grpc-reflection-js": "^0.3.0",
|
||||
"is-ip": "^5.0.1",
|
||||
"system-ca": "^2.0.1",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"ws": "^8.18.3"
|
||||
"tough-cookie": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.22.0",
|
||||
@@ -32126,29 +31899,6 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-requests/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/bruno-requests/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-requests/node_modules/tough-cookie": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
|
||||
@@ -32160,27 +31910,6 @@
|
||||
"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",
|
||||
@@ -32225,8 +31954,7 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"ws": "^8.18.3"
|
||||
"multer": "^1.4.5-lts.1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-tests/node_modules/axios": {
|
||||
@@ -32346,27 +32074,6 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-tests/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-toml": {
|
||||
"name": "@usebruno/toml",
|
||||
"version": "0.1.0",
|
||||
|
||||
14
package.json
14
package.json
@@ -19,25 +19,20 @@
|
||||
],
|
||||
"homepage": "https://usebruno.com",
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.3.2",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@stylistic/eslint-plugin": "^5.3.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.14.1",
|
||||
"@typescript-eslint/parser": "^8.39.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-plugin-diff": "^2.0.3",
|
||||
"fs-extra": "^11.1.1",
|
||||
"globals": "^16.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nano-staged": "^0.8.0",
|
||||
"playwright": "^1.51.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
@@ -73,14 +68,7 @@
|
||||
"test:e2e": "playwright test --project=default",
|
||||
"test:e2e:ssl": "playwright test --project=ssl",
|
||||
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
|
||||
"lint": "node --max_old_space_size=4096 $(npx which eslint)",
|
||||
"lint:fix": "node --max_old_space_size=4096 $(npx which eslint) --fix",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"nano-staged": {
|
||||
"*.{js,ts,jsx}": [
|
||||
"npm run lint:fix"
|
||||
]
|
||||
"lint": "node --max_old_space_size=4096 $(npx which eslint)"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.29.5",
|
||||
|
||||
@@ -109,17 +109,6 @@ const StyledWrapper = styled.div`
|
||||
text-decoration:unset;
|
||||
}
|
||||
|
||||
.cm-search-line-highlight {
|
||||
background: ${(props) => props.theme.codemirror.searchLineHighlightCurrent};
|
||||
}
|
||||
|
||||
.cm-search-match {
|
||||
background: rgba(255, 193, 7, 0.25);
|
||||
}
|
||||
|
||||
.cm-search-current {
|
||||
background: rgba(255, 193, 7, 0.4);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -14,7 +14,6 @@ import * as jsonlint from '@prantlf/jsonlint';
|
||||
import { JSHINT } from 'jshint';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import CodeMirrorSearch from 'components/CodeMirrorSearch';
|
||||
|
||||
const CodeMirror = require('codemirror');
|
||||
window.jsonlint = jsonlint;
|
||||
@@ -38,10 +37,6 @@ export default class CodeEditor extends React.Component {
|
||||
expr: true,
|
||||
asi: true
|
||||
};
|
||||
|
||||
this.state = {
|
||||
searchBarVisible: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -50,7 +45,7 @@ export default class CodeEditor extends React.Component {
|
||||
const editor = (this.editor = CodeMirror(this._node, {
|
||||
value: this.props.value || '',
|
||||
lineNumbers: true,
|
||||
lineWrapping: this.props.enableLineWrapping ?? true,
|
||||
lineWrapping: true,
|
||||
tabSize: TAB_SIZE,
|
||||
mode: this.props.mode || 'application/ld+json',
|
||||
brunoVarInfo: {
|
||||
@@ -88,14 +83,24 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
},
|
||||
'Cmd-F': (cm) => {
|
||||
if (!this.state.searchBarVisible) {
|
||||
this.setState({ searchBarVisible: true });
|
||||
if (this._isSearchOpen()) {
|
||||
// replace the older search component with the new one
|
||||
const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
search && search.remove();
|
||||
}
|
||||
cm.execCommand('findPersistent');
|
||||
this._bindSearchHandler();
|
||||
this._appendSearchResultsCount();
|
||||
},
|
||||
'Ctrl-F': (cm) => {
|
||||
if (!this.state.searchBarVisible) {
|
||||
this.setState({ searchBarVisible: true });
|
||||
if (this._isSearchOpen()) {
|
||||
// replace the older search component with the new one
|
||||
const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
search && search.remove();
|
||||
}
|
||||
cm.execCommand('findPersistent');
|
||||
this._bindSearchHandler();
|
||||
this._appendSearchResultsCount();
|
||||
},
|
||||
'Cmd-H': 'replace',
|
||||
'Ctrl-H': 'replace',
|
||||
@@ -124,11 +129,6 @@ export default class CodeEditor extends React.Component {
|
||||
} else {
|
||||
this.editor.toggleComment();
|
||||
}
|
||||
},
|
||||
'Esc': () => {
|
||||
if (this.state.searchBarVisible) {
|
||||
this.setState({ searchBarVisible: false });
|
||||
}
|
||||
}
|
||||
},
|
||||
foldOptions: {
|
||||
@@ -237,14 +237,6 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.scrollTo(null, this.props.initialScroll);
|
||||
}
|
||||
|
||||
if (this.props.enableLineWrapping !== prevProps.enableLineWrapping) {
|
||||
this.editor.setOption('lineWrapping', this.props.enableLineWrapping);
|
||||
}
|
||||
|
||||
if (this.props.mode !== prevProps.mode) {
|
||||
this.editor.setOption('mode', this.props.mode);
|
||||
}
|
||||
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
@@ -254,6 +246,11 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.off('scroll', this.onScroll);
|
||||
this.editor = null;
|
||||
}
|
||||
|
||||
this._unbindSearchHandler();
|
||||
if (this.brunoAutoCompleteCleanup) {
|
||||
this.brunoAutoCompleteCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -266,18 +263,10 @@ export default class CodeEditor extends React.Component {
|
||||
aria-label="Code Editor"
|
||||
font={this.props.font}
|
||||
fontSize={this.props.fontSize}
|
||||
>
|
||||
<CodeMirrorSearch
|
||||
visible={this.state.searchBarVisible}
|
||||
editor={this.editor}
|
||||
onClose={() => this.setState({ searchBarVisible: false })}
|
||||
/>
|
||||
<div
|
||||
className={`editor-container${this.state.searchBarVisible ? ' search-bar-visible' : ''}`}
|
||||
ref={(node) => { this._node = node; }}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -301,4 +290,67 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_isSearchOpen = () => {
|
||||
return document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
};
|
||||
|
||||
/**
|
||||
* Bind handler to search input to count number of search results
|
||||
*/
|
||||
_bindSearchHandler = () => {
|
||||
const searchInput = document.querySelector('.CodeMirror-search-field');
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', this._countSearchResults);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Unbind handler to search input to count number of search results
|
||||
*/
|
||||
_unbindSearchHandler = () => {
|
||||
const searchInput = document.querySelector('.CodeMirror-search-field');
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.removeEventListener('input', this._countSearchResults);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Append search results count to search dialog
|
||||
*/
|
||||
_appendSearchResultsCount = () => {
|
||||
const dialog = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
|
||||
if (dialog) {
|
||||
const searchResultsCount = document.createElement('span');
|
||||
searchResultsCount.id = this.searchResultsCountElementId;
|
||||
dialog.appendChild(searchResultsCount);
|
||||
|
||||
this._countSearchResults();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Count search results and update state
|
||||
*/
|
||||
_countSearchResults = () => {
|
||||
let count = 0;
|
||||
|
||||
const searchInput = document.querySelector('.CodeMirror-search-field');
|
||||
|
||||
if (searchInput && searchInput.value.length > 0) {
|
||||
// Escape special characters in search input to prevent RegExp crashes. Fixes #3051
|
||||
const text = new RegExp(escapeRegExp(searchInput.value), 'gi');
|
||||
const matches = this.editor.getValue().match(text);
|
||||
count = matches ? matches.length : 0;
|
||||
}
|
||||
|
||||
const searchResultsCountElement = document.querySelector(`#${this.searchResultsCountElementId}`);
|
||||
|
||||
if (searchResultsCountElement) {
|
||||
searchResultsCountElement.innerText = `${count} results`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.bruno-search-bar {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
padding: 0 2px;
|
||||
min-height: 36px;
|
||||
background: ${(props) => props.theme.sidebar.search.bg} !important;
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${(props) => props.theme.sidebar.search.bg} !important;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
width: auto;
|
||||
min-width: 180px;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.bruno-search-bar input {
|
||||
min-width: 80px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 1px 2px;
|
||||
font-size: 13px;
|
||||
margin: 0 1px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.searchbar-icon-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0 1px;
|
||||
margin: 0 1px;
|
||||
cursor: pointer;
|
||||
color: #aaa;
|
||||
border-radius: 3px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.searchbar-result-count {
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: #aaa;
|
||||
margin: 0 8px 0 1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bruno-search-bar.compact {
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
color: ${(props) => props.theme.codemirror.text || props.theme.text};
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
border-radius: 4px;
|
||||
padding: 1px 3px;
|
||||
min-height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.bruno-search-bar input {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
padding: 1px 2px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.searchbar-icon-btn:focus {
|
||||
outline: 1px solid ${(props) => props.theme.codemirror.border};
|
||||
}
|
||||
|
||||
.bruno-search-bar, .bruno-search-bar input {
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
||||
}
|
||||
|
||||
.cm-search-line-highlight {
|
||||
background: ${(props) => props.theme.codemirror.searchLineHighlightCurrent};
|
||||
}
|
||||
|
||||
.searchbar-icon-btn.active {
|
||||
color: #f39c12 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,201 +0,0 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { IconRegex, IconArrowUp, IconArrowDown, IconX, IconLetterCase, IconLetterW } from '@tabler/icons';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');
|
||||
}
|
||||
|
||||
const CodeMirrorSearch = ({ visible, editor, onClose }) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [regex, setRegex] = useState(false);
|
||||
const [caseSensitive, setCaseSensitive] = useState(false);
|
||||
const [wholeWord, setWholeWord] = useState(false);
|
||||
const [matchIndex, setMatchIndex] = useState(0);
|
||||
const [matchCount, setMatchCount] = useState(0);
|
||||
|
||||
const searchMarks = useRef([]);
|
||||
const searchLineHighlight = useRef(null);
|
||||
const searchMatches = useRef([]);
|
||||
|
||||
const debouncedSearchText = useDebounce(searchText, 150);
|
||||
|
||||
const memoizedMatches = useMemo(() => {
|
||||
if (!editor || !visible) return [];
|
||||
if (!debouncedSearchText) return [];
|
||||
|
||||
try {
|
||||
let query, options = {};
|
||||
if (regex) {
|
||||
try {
|
||||
query = new RegExp(debouncedSearchText, caseSensitive ? 'g' : 'gi');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
} else if (wholeWord) {
|
||||
const escaped = escapeRegExp(debouncedSearchText);
|
||||
query = new RegExp(`\\b${escaped}\\b`, caseSensitive ? 'g' : 'gi');
|
||||
} else {
|
||||
query = debouncedSearchText;
|
||||
options = { caseFold: !caseSensitive };
|
||||
}
|
||||
|
||||
const cursor = editor.getSearchCursor(query, { line: 0, ch: 0 }, options);
|
||||
const out = [];
|
||||
while (cursor.findNext()) {
|
||||
out.push({ from: cursor.from(), to: cursor.to() });
|
||||
}
|
||||
return out;
|
||||
} catch (e) {
|
||||
console.error('Search error:', e);
|
||||
return [];
|
||||
}
|
||||
}, [editor, visible, debouncedSearchText, regex, caseSensitive, wholeWord]);
|
||||
|
||||
const doSearch = useCallback((newIndex = 0) => {
|
||||
if (!editor) return;
|
||||
|
||||
// Clear previous marks
|
||||
searchMarks.current.forEach((mark) => mark.clear());
|
||||
searchMarks.current = [];
|
||||
// Clear previous line highlight
|
||||
if (searchLineHighlight.current !== null) {
|
||||
editor.removeLineClass(searchLineHighlight.current, 'wrap', 'cm-search-line-highlight');
|
||||
searchLineHighlight.current = null;
|
||||
}
|
||||
|
||||
if (!debouncedSearchText) {
|
||||
setMatchCount(0);
|
||||
setMatchIndex(0);
|
||||
searchMatches.current = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const matches = memoizedMatches;
|
||||
let matchIndex = matches.length ? Math.max(0, Math.min(newIndex, matches.length - 1)) : 0;
|
||||
matches.forEach((m, i) => {
|
||||
const mark = editor.markText(m.from, m.to, {
|
||||
className: i === matchIndex ? 'cm-search-current' : 'cm-search-match',
|
||||
clearOnEnter: true
|
||||
});
|
||||
searchMarks.current.push(mark);
|
||||
});
|
||||
|
||||
if (matches.length) {
|
||||
const currentLine = matches[matchIndex].from.line;
|
||||
editor.addLineClass(currentLine, 'wrap', 'cm-search-line-highlight');
|
||||
searchLineHighlight.current = currentLine;
|
||||
|
||||
editor.scrollIntoView(matches[matchIndex].from, 100);
|
||||
editor.setSelection(matches[matchIndex].from, matches[matchIndex].to);
|
||||
} else {
|
||||
searchLineHighlight.current = null;
|
||||
}
|
||||
|
||||
setMatchCount(matches.length);
|
||||
setMatchIndex(matchIndex);
|
||||
searchMatches.current = matches;
|
||||
} catch (e) {
|
||||
console.error('Search error:', e);
|
||||
setMatchCount(0);
|
||||
setMatchIndex(0);
|
||||
searchMatches.current = [];
|
||||
}
|
||||
}, [debouncedSearchText, regex, caseSensitive, wholeWord, editor, memoizedMatches]);
|
||||
|
||||
useEffect(() => {
|
||||
doSearch(0, debouncedSearchText);
|
||||
}, [debouncedSearchText, doSearch]);
|
||||
|
||||
const handleSearchBarClose = useCallback(() => {
|
||||
searchMarks.current.forEach((mark) => mark.clear());
|
||||
searchMarks.current = [];
|
||||
if (searchLineHighlight.current !== null && editor) {
|
||||
editor.removeLineClass(searchLineHighlight.current, 'wrap', 'cm-search-line-highlight');
|
||||
searchLineHighlight.current = null;
|
||||
}
|
||||
searchMatches.current = [];
|
||||
if (onClose) onClose();
|
||||
// Focus the editor after closing the search bar
|
||||
if (editor) {
|
||||
setTimeout(() => editor.focus(), 0);
|
||||
}
|
||||
}, [editor, onClose]);
|
||||
|
||||
const handleSearchTextChange = (text) => {
|
||||
setSearchText(text);
|
||||
setMatchIndex(0);
|
||||
};
|
||||
|
||||
const handleToggleRegex = () => {
|
||||
setRegex((prev) => !prev);
|
||||
setMatchIndex(0);
|
||||
doSearch(0);
|
||||
};
|
||||
|
||||
const handleToggleCase = () => {
|
||||
setCaseSensitive((prev) => !prev);
|
||||
setMatchIndex(0);
|
||||
doSearch(0);
|
||||
};
|
||||
|
||||
const handleToggleWholeWord = () => {
|
||||
setWholeWord((prev) => !prev);
|
||||
setMatchIndex(0);
|
||||
doSearch(0);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (!searchMatches.current || !searchMatches.current.length) return;
|
||||
let next = (matchIndex + 1) % searchMatches.current.length;
|
||||
setMatchIndex(next);
|
||||
doSearch(next);
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
if (!searchMatches.current || !searchMatches.current.length) return;
|
||||
let prev = (matchIndex - 1 + searchMatches.current.length) % searchMatches.current.length;
|
||||
setMatchIndex(prev);
|
||||
doSearch(prev);
|
||||
};
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="bruno-search-bar compact">
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
value={searchText}
|
||||
onChange={(e) => handleSearchTextChange(e.target.value)}
|
||||
placeholder="Search..."
|
||||
spellCheck={false}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) handleNext();
|
||||
if (e.key === 'Enter' && e.shiftKey) handlePrev();
|
||||
if (e.key === 'Escape') handleSearchBarClose();
|
||||
}}
|
||||
/>
|
||||
<span className="searchbar-result-count">{matchCount > 0 ? `${matchIndex + 1} / ${matchCount}` : '0 results'}</span>
|
||||
<ToolHint text="Regex search" toolhintId="searchbar-regex-toolhint" place="top">
|
||||
<button className={`searchbar-icon-btn ${regex ? 'active' : ''}`} onClick={handleToggleRegex}><IconRegex size={16} /></button>
|
||||
</ToolHint>
|
||||
<ToolHint text="Case sensitive" toolhintId="searchbar-case-toolhint" place="top">
|
||||
<button className={`searchbar-icon-btn ${caseSensitive ? 'active' : ''}`} onClick={handleToggleCase}><IconLetterCase size={14} /></button>
|
||||
</ToolHint>
|
||||
<ToolHint text="Whole word" toolhintId="searchbar-wholeword-toolhint" place="top">
|
||||
<button className={`searchbar-icon-btn ${wholeWord ? 'active' : ''}`} onClick={handleToggleWholeWord}><IconLetterW size={14} /></button>
|
||||
</ToolHint>
|
||||
<button className="searchbar-icon-btn" title="Previous" onClick={handlePrev}><IconArrowUp size={14} /></button>
|
||||
<button className="searchbar-icon-btn" title="Next" onClick={handleNext}><IconArrowDown size={14} /></button>
|
||||
<button className="searchbar-icon-btn" title="Close" onClick={handleSearchBarClose}><IconX size={14} /></button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeMirrorSearch;
|
||||
@@ -10,4 +10,4 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,263 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash, IconFile, IconFileImport, IconAlertCircle } from '@tabler/icons';
|
||||
import { getRelativePath, getBasename, getDirPath } from 'utils/common/path';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { existsSync, resolvePath } from '../../../utils/filesystem';
|
||||
|
||||
const GrpcSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
brunoConfig: { grpc: grpcConfig = {} }
|
||||
} = collection;
|
||||
|
||||
const fileInputRef = useRef(null);
|
||||
const [protoFileValidity, setProtoFileValidity] = useState({});
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
protoFiles: grpcConfig.protoFiles || []
|
||||
},
|
||||
onSubmit: (newGrpcConfig) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.grpc = newGrpcConfig;
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid));
|
||||
toast.success('gRPC settings updated');
|
||||
}
|
||||
});
|
||||
|
||||
// Get file path using the ipcRenderer
|
||||
const getProtoFile = (event) => {
|
||||
const files = event?.files;
|
||||
if (files && files.length > 0) {
|
||||
const newProtoFiles = [...formik.values.protoFiles];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(files[i]);
|
||||
if (filePath) {
|
||||
const relativePath = getRelativePath(filePath, collection.pathname);
|
||||
const protoFileObj = {
|
||||
path: relativePath,
|
||||
type: 'file'
|
||||
};
|
||||
|
||||
// Check if this path already exists
|
||||
const exists = newProtoFiles.some(pf => pf.path === protoFileObj.path);
|
||||
if (!exists) {
|
||||
newProtoFiles.push(protoFileObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formik.setFieldValue('protoFiles', newProtoFiles);
|
||||
// Reset the file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for removing a proto file
|
||||
const handleRemoveProtoFile = (index) => {
|
||||
const updatedProtoFiles = [...formik.values.protoFiles];
|
||||
updatedProtoFiles.splice(index, 1);
|
||||
formik.setFieldValue('protoFiles', updatedProtoFiles);
|
||||
};
|
||||
|
||||
// Handle the browse button click
|
||||
const handleBrowseClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
// Check if a proto file path is valid
|
||||
const isProtoFileValid = async (protoFile) => {
|
||||
try {
|
||||
const absolutePath = await resolvePath(protoFile.path, collection.pathname);
|
||||
return await existsSync(absolutePath);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Validate all proto files and update state
|
||||
useEffect(() => {
|
||||
const validateProtoFiles = async () => {
|
||||
const validityMap = {};
|
||||
for (const file of formik.values.protoFiles) {
|
||||
validityMap[file.path] = await isProtoFileValid(file);
|
||||
}
|
||||
setProtoFileValidity(validityMap);
|
||||
};
|
||||
|
||||
validateProtoFiles();
|
||||
}, [formik.values.protoFiles, collection.pathname]);
|
||||
|
||||
// Handle replacing an invalid proto file
|
||||
const handleReplaceProtoFile = (index) => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
// Store the index to replace after file selection
|
||||
fileInputRef.current.dataset.replaceIndex = index;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle file input change
|
||||
const handleFileInputChange = (e) => {
|
||||
const replaceIndex = e.target.dataset.replaceIndex;
|
||||
if (replaceIndex !== undefined) {
|
||||
// Handle replacement
|
||||
const files = e.target.files;
|
||||
if (files && files.length > 0) {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(files[0]);
|
||||
if (filePath) {
|
||||
const relativePath = getRelativePath(filePath, collection.pathname);
|
||||
const updatedProtoFiles = [...formik.values.protoFiles];
|
||||
updatedProtoFiles[replaceIndex] = {
|
||||
path: relativePath,
|
||||
type: 'file'
|
||||
};
|
||||
formik.setFieldValue('protoFiles', updatedProtoFiles);
|
||||
}
|
||||
}
|
||||
delete e.target.dataset.replaceIndex;
|
||||
} else {
|
||||
getProtoFile(e.target);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label className="font-semibold text-sm mb-3 flex items-center" htmlFor="protoFiles">
|
||||
Add Proto Files
|
||||
<span id="proto-files-tooltip" className="ml-2">
|
||||
<IconAlertCircle size={16} className="text-gray-500 cursor-pointer" />
|
||||
</span>
|
||||
<Tooltip
|
||||
anchorId="proto-files-tooltip"
|
||||
className="tooltip-mod font-normal"
|
||||
html="Keep your proto files within the collection folder or the corresponding git repository to ensure paths remain valid when sharing the collection."
|
||||
/>
|
||||
</label>
|
||||
<div className="flex flex-col">
|
||||
{/* Hidden file input for file selection */}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
accept=".proto"
|
||||
multiple
|
||||
onChange={handleFileInputChange}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* File selection options */}
|
||||
<div className="flex flex-col space-y-3">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary flex items-center"
|
||||
onClick={handleBrowseClick}
|
||||
>
|
||||
<IconFileImport size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Browse for proto files
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="border-t border-neutral-600 my-2"></div>
|
||||
|
||||
{/* List of added proto files */}
|
||||
<div>
|
||||
<div className="text-sm font-semibold mb-2 flex items-center">
|
||||
<IconFile size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Added Proto Files ({formik.values.protoFiles.length})
|
||||
</div>
|
||||
|
||||
{formik.values.protoFiles.length === 0 ? (
|
||||
<div className="text-neutral-500 text-sm italic">No proto files added yet</div>
|
||||
) : (
|
||||
<>
|
||||
{formik.values.protoFiles.some(file => !protoFileValidity[file.path]) && (
|
||||
<div className="text-xs text-red-500 mb-2 flex items-center bg-red-50 dark:bg-red-900/20 p-2 rounded">
|
||||
<IconAlertCircle size={14} className="mr-1" />
|
||||
Some proto files cannot be found at their specified paths. Use the "Replace" option to update their locations.
|
||||
</div>
|
||||
)}
|
||||
<ul className="mt-4">
|
||||
{formik.values.protoFiles.map((file, index) => {
|
||||
const isValid = protoFileValidity[file.path];
|
||||
return (
|
||||
<li key={index} className="flex items-center available-certificates p-2 rounded-lg mb-2">
|
||||
<div className="flex items-center w-full justify-between">
|
||||
<div className="flex w-full items-center">
|
||||
<IconFile className="mr-2" size={18} strokeWidth={1.5} />
|
||||
<div
|
||||
className="overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px] text-sm"
|
||||
title={file.path}
|
||||
>
|
||||
{getBasename(file.path)}
|
||||
<span className="text-xs text-neutral-500 ml-2">
|
||||
{getDirPath(file.path)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-end">
|
||||
{!isValid && (
|
||||
<div className="flex items-center mr-2">
|
||||
<IconAlertCircle
|
||||
size={16}
|
||||
className="text-red-500"
|
||||
title="Proto file not found. Click to replace."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-red-500 ml-1 hover:underline"
|
||||
onClick={() => handleReplaceProtoFile(index)}
|
||||
>
|
||||
Replace
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="remove-certificate ml-2"
|
||||
onClick={() => handleRemoveProtoFile(index)}
|
||||
title="Remove file"
|
||||
>
|
||||
<IconTrash size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrpcSettings;
|
||||
@@ -5,9 +5,11 @@ import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
|
||||
|
||||
const PresetsSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isGrpcEnabled = useBetaFeature(BETA_FEATURES.GRPC);
|
||||
const {
|
||||
brunoConfig: { presets: presets = {} }
|
||||
} = collection;
|
||||
@@ -15,10 +17,15 @@ const PresetsSettings = ({ collection }) => {
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
requestType: presets.requestType || 'http',
|
||||
requestType: presets.requestType === 'grpc' && !isGrpcEnabled ? 'http' : presets.requestType || 'http',
|
||||
requestUrl: presets.requestUrl || ''
|
||||
},
|
||||
onSubmit: (newPresets) => {
|
||||
// If gRPC is disabled but the preset is set to grpc, change it to http
|
||||
if (!isGrpcEnabled && newPresets.requestType === 'grpc') {
|
||||
newPresets.requestType = 'http';
|
||||
}
|
||||
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.presets = newPresets;
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid));
|
||||
@@ -63,18 +70,22 @@ const PresetsSettings = ({ collection }) => {
|
||||
GraphQL
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="grpc"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
value="grpc"
|
||||
checked={formik.values.requestType === 'grpc'}
|
||||
/>
|
||||
<label htmlFor="grpc" className="ml-1 cursor-pointer select-none">
|
||||
gRPC
|
||||
</label>
|
||||
{isGrpcEnabled && (
|
||||
<>
|
||||
<input
|
||||
id="grpc"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
value="grpc"
|
||||
checked={formik.values.requestType === 'grpc'}
|
||||
/>
|
||||
<label htmlFor="grpc" className="ml-1 cursor-pointer select-none">
|
||||
gRPC
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
|
||||
@@ -1,336 +0,0 @@
|
||||
import React, { useRef } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import {
|
||||
IconTrash,
|
||||
IconFile,
|
||||
IconFileImport,
|
||||
IconAlertCircle,
|
||||
IconFolder
|
||||
} from '@tabler/icons';
|
||||
import { getBasename } from 'utils/common/path';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import useProtoFileManagement from '../../../hooks/useProtoFileManagement';
|
||||
|
||||
const ProtobufSettings = ({ collection }) => {
|
||||
const {
|
||||
protoFiles,
|
||||
importPaths,
|
||||
addProtoFileToCollection,
|
||||
addImportPathToCollection,
|
||||
toggleImportPath,
|
||||
browseForProtoFile,
|
||||
browseForImportDirectory,
|
||||
removeProtoFileFromCollection,
|
||||
removeImportPathFromCollection,
|
||||
replaceImportPathInCollection,
|
||||
replaceProtoFileInCollection
|
||||
} = useProtoFileManagement(collection);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
// Get file path using the ipcRenderer
|
||||
const getProtoFile = async (event) => {
|
||||
const files = event?.files;
|
||||
if (files && files.length > 0) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(files[i]);
|
||||
if (filePath) {
|
||||
await addProtoFileToCollection(filePath);
|
||||
}
|
||||
}
|
||||
// Reset the file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveProtoFile = async (index) => {
|
||||
await removeProtoFileFromCollection(index);
|
||||
};
|
||||
|
||||
const handleBrowseClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleReplaceProtoFile = async (index) => {
|
||||
const result = await browseForProtoFile();
|
||||
if (result.success) {
|
||||
await replaceProtoFileInCollection(index, result.filePath);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReplaceImportPath = async (index) => {
|
||||
const result = await browseForImportDirectory();
|
||||
if (result.success) {
|
||||
await replaceImportPathInCollection(index, result.directoryPath);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileInputChange = (e) => {
|
||||
getProtoFile(e.target);
|
||||
};
|
||||
|
||||
const getImportPath = async () => {
|
||||
const result = await browseForImportDirectory();
|
||||
if (result.success) {
|
||||
await addImportPathToCollection(result.directoryPath);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveImportPath = async (index) => {
|
||||
await removeImportPathFromCollection(index);
|
||||
};
|
||||
|
||||
const handleToggleImportPath = async (index) => {
|
||||
await toggleImportPath(index);
|
||||
};
|
||||
|
||||
const handleBrowseImportPathClick = () => {
|
||||
getImportPath();
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
{/* Hidden file input for file selection */}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
accept=".proto"
|
||||
multiple
|
||||
onChange={handleFileInputChange}
|
||||
/>
|
||||
|
||||
{/* Proto Files Section */}
|
||||
<div className="mb-6" data-testid="protobuf-proto-files-section">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center">
|
||||
<label className="font-semibold text-sm flex items-center" htmlFor="protoFiles">
|
||||
Proto Files (
|
||||
{protoFiles.length}
|
||||
)
|
||||
<span id="proto-files-tooltip" className="ml-2">
|
||||
<IconAlertCircle size={16} className="text-gray-500 cursor-pointer" />
|
||||
</span>
|
||||
<Tooltip
|
||||
anchorId="proto-files-tooltip"
|
||||
className="tooltip-mod font-normal"
|
||||
html="Keep your proto files within the collection folder or the corresponding git repository to ensure paths remain valid when sharing the collection."
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{protoFiles.some((file) => !file.exists) && (
|
||||
<div className="text-xs text-red-600 dark:text-red-400 mb-2 flex items-center p-2 rounded" data-testid="protobuf-invalid-files-message">
|
||||
<IconAlertCircle size={14} className="mr-1" />
|
||||
Some proto files cannot be found. Use the replace option to update their locations.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<table className="w-full border-collapse" data-testid="protobuf-proto-files-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
File
|
||||
</th>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
Path
|
||||
</th>
|
||||
<th className="text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{protoFiles.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="3" className="border border-gray-200 dark:border-gray-700 px-3 py-8 text-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<IconFile size={24} className="text-gray-400 mb-2" />
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">No proto files added</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
protoFiles.map((file, index) => {
|
||||
const isValid = file.exists;
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<div className="flex items-center">
|
||||
<IconFile size={16} className="text-gray-500 dark:text-gray-400 mr-2" />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{getBasename(collection.pathname, file.path)}
|
||||
</span>
|
||||
{!isValid && <IconAlertCircle size={12} className="text-red-600 dark:text-red-400 ml-2" />}
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 font-mono">
|
||||
{file.path}
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{!isValid && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReplaceProtoFile(index)}
|
||||
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 p-1 rounded"
|
||||
title="Replace file"
|
||||
>
|
||||
<IconFileImport size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveProtoFile(index)}
|
||||
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 p-1 rounded"
|
||||
title="Remove file"
|
||||
data-testid="protobuf-remove-file-button"
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleBrowseClick} data-testid="protobuf-add-file-button">
|
||||
+ Add Proto File
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Import Paths Section */}
|
||||
<div className="mb-6" data-testid="protobuf-import-paths-section">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center">
|
||||
<label className="font-semibold text-sm flex items-center" htmlFor="importPaths">
|
||||
Import Paths (
|
||||
{importPaths.length}
|
||||
)
|
||||
<span id="import-paths-tooltip" className="ml-2">
|
||||
<IconAlertCircle size={16} className="text-gray-500 cursor-pointer" />
|
||||
</span>
|
||||
<Tooltip
|
||||
anchorId="import-paths-tooltip"
|
||||
className="tooltip-mod font-normal"
|
||||
html="Add directories that contain proto files to be imported. These paths help resolve import statements in your proto files."
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{importPaths.some((path) => !path.exists) && (
|
||||
<div className="text-xs text-red-600 dark:text-red-400 mb-2 flex items-center p-2 rounded" data-testid="protobuf-invalid-import-paths-message">
|
||||
<IconAlertCircle size={14} className="mr-1" />
|
||||
Some import paths cannot be found at their specified locations.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<table className="w-full border-collapse" data-testid="protobuf-import-paths-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
</th>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
Directory
|
||||
</th>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
Path
|
||||
</th>
|
||||
<th className="text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{importPaths.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="4" className="border border-gray-200 dark:border-gray-700 px-3 py-8 text-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<IconFolder size={24} className="text-gray-400 mb-2" />
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">No import paths added</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
importPaths.map((importPath, index) => {
|
||||
const isValid = importPath.exists;
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={importPath.enabled}
|
||||
onChange={() => handleToggleImportPath(index)}
|
||||
className="h-4 w-4 text-gray-600 focus:ring-gray-500 border-gray-300 dark:border-gray-600 rounded"
|
||||
title={importPath.enabled ? 'Disable this import path' : 'Enable this import path'}
|
||||
data-testid="protobuf-import-path-checkbox"
|
||||
/>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<div className="flex items-center">
|
||||
<IconFolder size={16} className="text-gray-500 dark:text-gray-400 mr-2" />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{getBasename(collection.pathname, importPath.path)}
|
||||
</span>
|
||||
{!isValid && <IconAlertCircle size={12} className="text-red-600 dark:text-red-400 ml-2" />}
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 font-mono">
|
||||
{importPath.path}
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{!isValid && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReplaceImportPath(index)}
|
||||
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 p-1 rounded"
|
||||
title="Replace directory"
|
||||
>
|
||||
<IconFileImport size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveImportPath(index)}
|
||||
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 p-1 rounded"
|
||||
title="Remove import path"
|
||||
data-testid="protobuf-remove-import-path-button"
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleBrowseImportPathClick} data-testid="protobuf-add-import-path-button">
|
||||
+ Add Import Path
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProtobufSettings;
|
||||
@@ -13,14 +13,16 @@ import Auth from './Auth';
|
||||
import Script from './Script';
|
||||
import Test from './Tests';
|
||||
import Presets from './Presets';
|
||||
import Protobuf from './Protobuf';
|
||||
import Grpc from './Grpc';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars/index';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
import Overview from './Overview/index';
|
||||
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
|
||||
|
||||
const CollectionSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isGrpcEnabled = useBetaFeature(BETA_FEATURES.GRPC);
|
||||
const tab = collection.settingsSelectedTab;
|
||||
const setTab = (tab) => {
|
||||
dispatch(
|
||||
@@ -44,13 +46,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
const authMode = get(collection, 'root.request.auth', {}).mode || 'none';
|
||||
|
||||
const presets = get(collection, 'brunoConfig.presets', []);
|
||||
const hasPresets = presets && presets.requestUrl !== "";
|
||||
|
||||
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
||||
const proxyEnabled = proxyConfig.hostname ? true : false;
|
||||
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
const protobufConfig = get(collection, 'brunoConfig.protobuf', {});
|
||||
const grpcConfig = get(collection, 'brunoConfig.grpc', {});
|
||||
|
||||
const onProxySettingsUpdate = (config) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
@@ -127,8 +125,8 @@ const CollectionSettings = ({ collection }) => {
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'protobuf': {
|
||||
return <Protobuf collection={collection} />;
|
||||
case 'grpc': {
|
||||
return <Grpc collection={collection} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -167,20 +165,21 @@ const CollectionSettings = ({ collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('presets')} role="tab" onClick={() => setTab('presets')}>
|
||||
Presets
|
||||
{hasPresets && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||
Proxy
|
||||
{Object.keys(proxyConfig).length > 0 && proxyEnabled && <StatusDot />}
|
||||
{Object.keys(proxyConfig).length > 0 && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('clientCert')} role="tab" onClick={() => setTab('clientCert')}>
|
||||
Client Certificates
|
||||
{clientCertConfig.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('protobuf')} role="tab" onClick={() => setTab('protobuf')}>
|
||||
Protobuf
|
||||
{protobufConfig.protoFiles && protobufConfig.protoFiles.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
{isGrpcEnabled && (
|
||||
<div className={getTabClassname('grpc')} role="tab" onClick={() => setTab('grpc')}>
|
||||
gRPC
|
||||
{grpcConfig.protoFiles && grpcConfig.protoFiles.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<section className="mt-4 h-full overflow-auto">{getTabPanel(tab)}</section>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
IconCode,
|
||||
IconChevronDown,
|
||||
IconTerminal2,
|
||||
IconNetwork,
|
||||
IconDashboard,
|
||||
IconNetwork
|
||||
} from '@tabler/icons';
|
||||
import {
|
||||
closeConsole,
|
||||
@@ -25,12 +24,10 @@ import {
|
||||
updateNetworkFilter,
|
||||
toggleAllNetworkFilters
|
||||
} from 'providers/ReduxStore/slices/logs';
|
||||
|
||||
import NetworkTab from './NetworkTab';
|
||||
import RequestDetailsPanel from './RequestDetailsPanel';
|
||||
// import DebugTab from './DebugTab';
|
||||
import ErrorDetailsPanel from './ErrorDetailsPanel';
|
||||
import Performance from '../Performance';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const LogIcon = ({ type }) => {
|
||||
@@ -387,8 +384,6 @@ const Console = () => {
|
||||
);
|
||||
case 'network':
|
||||
return <NetworkTab />;
|
||||
case 'performance':
|
||||
return <Performance />;
|
||||
// case 'debug':
|
||||
// return <DebugTab />;
|
||||
default:
|
||||
@@ -489,14 +484,6 @@ const Console = () => {
|
||||
<span>Network</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`console-tab ${activeTab === 'performance' ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange('performance')}
|
||||
>
|
||||
<IconDashboard size={16} strokeWidth={1.5} />
|
||||
<span>Performance</span>
|
||||
</button>
|
||||
|
||||
{/* <button
|
||||
className={`console-tab ${activeTab === 'debug' ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange('debug')}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.tab-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: ${props => props.theme.console.bg};
|
||||
}
|
||||
|
||||
.tab-content-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.overview-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.overview-section {
|
||||
margin-bottom: 32px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid ${props => props.theme.console.border};
|
||||
|
||||
h3 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: ${props => props.theme.console.titleColor};
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: ${props => props.theme.console.textMuted};
|
||||
}
|
||||
}
|
||||
|
||||
.system-resources {
|
||||
margin-bottom: 16px;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: ${props => props.theme.console.titleColor};
|
||||
}
|
||||
}
|
||||
|
||||
.resource-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.resource-card {
|
||||
background: ${props => props.theme.console.headerBg};
|
||||
border: 1px solid ${props => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.resource-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
color: ${props => props.theme.console.titleColor};
|
||||
}
|
||||
|
||||
.resource-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.resource-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: ${props => props.theme.console.titleColor};
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.resource-subtitle {
|
||||
font-size: 11px;
|
||||
color: ${props => props.theme.console.buttonColor};
|
||||
}
|
||||
|
||||
.resource-trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
margin-top: 8px;
|
||||
|
||||
&.up {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: #e81123;
|
||||
}
|
||||
|
||||
&.stable {
|
||||
color: ${props => props.theme.console.buttonColor};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,100 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import {
|
||||
IconCpu,
|
||||
IconDatabase,
|
||||
IconClock,
|
||||
IconServer,
|
||||
IconChartLine,
|
||||
} from '@tabler/icons';
|
||||
|
||||
const Performance = () => {
|
||||
const { systemResources } = useSelector(state => state.performance);
|
||||
|
||||
const formatBytes = bytes => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const formatUptime = seconds => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
if (hours > 0) return `${hours}h ${minutes}m ${secs}s`;
|
||||
if (minutes > 0) return `${minutes}m ${secs}s`;
|
||||
return `${secs}s`;
|
||||
};
|
||||
|
||||
const SystemResourceCard = ({ icon: Icon, title, value, subtitle, color = 'default', trend }) => (
|
||||
<div className={`resource-card ${color}`}>
|
||||
<div className="resource-header">
|
||||
<Icon size={20} strokeWidth={1.5} />
|
||||
<span className="resource-title">{title}</span>
|
||||
</div>
|
||||
<div className="resource-value">{value}</div>
|
||||
{subtitle && <div className="resource-subtitle">{subtitle}</div>}
|
||||
{trend && (
|
||||
<div className={`resource-trend ${trend > 0 ? 'up' : trend < 0 ? 'down' : 'stable'}`}>
|
||||
<IconChartLine size={12} strokeWidth={1.5} />
|
||||
<span>
|
||||
{trend > 0 ? '+' : ''}
|
||||
{trend.toFixed(1)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="tab-content">
|
||||
<div className="tab-content-area">
|
||||
<div className="system-resources">
|
||||
<h2>System Resources</h2>
|
||||
<div className="resource-cards">
|
||||
<SystemResourceCard
|
||||
icon={IconCpu}
|
||||
title="CPU Usage"
|
||||
value={`${systemResources.cpu.toFixed(1)}%`}
|
||||
subtitle="Current process"
|
||||
color={systemResources.cpu > 80 ? 'danger' : systemResources.cpu > 60 ? 'warning' : 'success'}
|
||||
/>
|
||||
|
||||
<SystemResourceCard
|
||||
icon={IconDatabase}
|
||||
title="Memory Usage"
|
||||
value={formatBytes(systemResources.memory)}
|
||||
subtitle="Current process"
|
||||
color={systemResources.memory > 500 * 1024 * 1024 ? 'danger' : 'default'}
|
||||
/>
|
||||
|
||||
<SystemResourceCard
|
||||
icon={IconClock}
|
||||
title="Uptime"
|
||||
value={formatUptime(systemResources.uptime)}
|
||||
subtitle="Process runtime"
|
||||
color="info"
|
||||
/>
|
||||
|
||||
<SystemResourceCard
|
||||
icon={IconServer}
|
||||
title="Process ID"
|
||||
value={systemResources.pid || 'N/A'}
|
||||
subtitle="Current PID"
|
||||
color="default"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Performance;
|
||||
@@ -2,12 +2,7 @@ import React from 'react';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Dropdown = ({ icon, children, onCreate, placement, transparent, visible, ...props }) => {
|
||||
// When in controlled mode (visible prop is provided), don't use trigger prop
|
||||
const tippyProps = visible !== undefined
|
||||
? { ...props, visible, interactive: true, appendTo: 'parent' }
|
||||
: { ...props, trigger: 'click', interactive: true, appendTo: 'parent' };
|
||||
|
||||
const Dropdown = ({ icon, children, onCreate, placement, transparent, ...props }) => {
|
||||
return (
|
||||
<StyledWrapper className="dropdown" transparent={transparent}>
|
||||
<Tippy
|
||||
@@ -16,7 +11,10 @@ const Dropdown = ({ icon, children, onCreate, placement, transparent, visible, .
|
||||
animation={false}
|
||||
arrow={false}
|
||||
onCreate={onCreate}
|
||||
{...tippyProps}
|
||||
interactive={true}
|
||||
trigger="click"
|
||||
appendTo="parent"
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
</Tippy>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { IconPlus, IconDownload, IconSettings } from '@tabler/icons';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
|
||||
const EnvironmentListContent = ({
|
||||
environments,
|
||||
@@ -19,30 +18,17 @@ const EnvironmentListContent = ({
|
||||
<div className="dropdown-item no-environment" onClick={() => onEnvironmentSelect(null)}>
|
||||
<span>No Environment</span>
|
||||
</div>
|
||||
<ToolHint
|
||||
anchorSelect="[data-tooltip-content]"
|
||||
place="right"
|
||||
positionStrategy="fixed"
|
||||
tooltipStyle={{
|
||||
maxWidth: '200px',
|
||||
wordWrap: 'break-word'
|
||||
}}
|
||||
delayShow={1000}
|
||||
>
|
||||
<div>
|
||||
{environments.map((env) => (
|
||||
<div
|
||||
key={env.uid}
|
||||
className={`dropdown-item ${env.uid === activeEnvironmentUid ? 'active' : ''}`}
|
||||
onClick={() => onEnvironmentSelect(env)}
|
||||
data-tooltip-content={env.name}
|
||||
data-tooltip-hidden={env.name?.length < 90}
|
||||
>
|
||||
<span className="max-w-100% truncate no-wrap">{env.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ToolHint>
|
||||
<div>
|
||||
{environments.map((env) => (
|
||||
<div
|
||||
key={env.uid}
|
||||
className={`dropdown-item ${env.uid === activeEnvironmentUid ? 'active' : ''}`}
|
||||
onClick={() => onEnvironmentSelect(env)}
|
||||
>
|
||||
<span className="max-w-32 truncate no-wrap">{env.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="dropdown-item configure-button">
|
||||
<button onClick={onSettingsClick} id="configure-env">
|
||||
<IconSettings size={16} strokeWidth={1.5} />
|
||||
|
||||
@@ -23,7 +23,6 @@ const Wrapper = styled.div`
|
||||
.env-text {
|
||||
color: ${(props) => props.theme.dropdown.selectedColor};
|
||||
font-size: 0.875rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.env-separator {
|
||||
@@ -46,10 +45,8 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.tippy-box {
|
||||
width: ${(props) => props.width}px;
|
||||
min-width: 12rem;
|
||||
max-width: 650px !important;
|
||||
min-height: 15.5rem;
|
||||
min-width: 11.875rem;
|
||||
min-height: 15.0625rem;
|
||||
max-height: 75vh;
|
||||
font-size: 0.8125rem;
|
||||
position: relative;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useState, useRef, forwardRef } from 'react';
|
||||
import React, { useState, useRef, forwardRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { IconWorld, IconDatabase, IconCaretDown, IconSettings, IconPlus, IconDownload } from '@tabler/icons';
|
||||
@@ -14,7 +14,6 @@ import CreateEnvironment from '../EnvironmentSettings/CreateEnvironment';
|
||||
import ImportEnvironment from '../EnvironmentSettings/ImportEnvironment';
|
||||
import CreateGlobalEnvironment from 'components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment';
|
||||
import ImportGlobalEnvironment from 'components/GlobalEnvironments/EnvironmentSettings/ImportEnvironment';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const EnvironmentSelector = ({ collection }) => {
|
||||
@@ -114,18 +113,6 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(false));
|
||||
};
|
||||
|
||||
// Calculate dropdown width based on the longest environment name.
|
||||
// To prevent resizing while switching between collection and global environments.
|
||||
const dropdownWidth = useMemo(() => {
|
||||
const allEnvironments = [...environments, ...globalEnvironments];
|
||||
if (allEnvironments.length === 0) return 0;
|
||||
|
||||
const maxCharLength = Math.max(...allEnvironments.map((env) => env.name?.length || 0));
|
||||
// 8 pixels per character: This is a rough estimate for the average character width in most fonts
|
||||
// (monospace fonts are typically 8-10px, proportional fonts vary but 8px is a safe average)
|
||||
return maxCharLength * 8;
|
||||
}, [environments, globalEnvironments]);
|
||||
|
||||
// Create icon component for dropdown trigger
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
const hasAnyEnv = activeGlobalEnvironment || activeCollectionEnvironment;
|
||||
@@ -136,15 +123,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<IconDatabase size={14} strokeWidth={1.5} className="env-icon" />
|
||||
<ToolHint
|
||||
text={activeCollectionEnvironment.name}
|
||||
toolhintId={`collection-env-${activeCollectionEnvironment.uid}`}
|
||||
place="bottom-start"
|
||||
delayShow={1000}
|
||||
hidden={activeCollectionEnvironment.name?.length < 7}
|
||||
>
|
||||
<span className="env-text max-w-24 truncate overflow-hidden">{activeCollectionEnvironment.name}</span>
|
||||
</ToolHint>
|
||||
<span className="env-text max-w-24 truncate no-wrap">{activeCollectionEnvironment.name}</span>
|
||||
</div>
|
||||
{activeGlobalEnvironment && <span className="env-separator">|</span>}
|
||||
</>
|
||||
@@ -152,15 +131,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
{activeGlobalEnvironment && (
|
||||
<div className="flex items-center">
|
||||
<IconWorld size={14} strokeWidth={1.5} className="env-icon" />
|
||||
<ToolHint
|
||||
text={activeGlobalEnvironment.name}
|
||||
toolhintId={`global-env-${activeGlobalEnvironment.uid}`}
|
||||
place="bottom-start"
|
||||
delayShow={1000}
|
||||
hidden={activeGlobalEnvironment.name?.length < 7}
|
||||
>
|
||||
<span className="env-text max-w-24 truncate overflow-hidden">{activeGlobalEnvironment.name}</span>
|
||||
</ToolHint>
|
||||
<span className="env-text max-w-24 truncate no-wrap">{activeGlobalEnvironment.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -183,11 +154,11 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper width={dropdownWidth}>
|
||||
<StyledWrapper>
|
||||
<div className="environment-selector flex align-center cursor-pointer">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
{/* Tab Headers */}
|
||||
<div className="tab-header flex p-[0.75rem]">
|
||||
<div className="tab-header flex justify-center p-[0.75rem]">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
@@ -222,12 +193,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
|
||||
{/* Modals - Rendered outside dropdown to avoid conflicts */}
|
||||
{showGlobalSettings && (
|
||||
<GlobalEnvironmentSettings
|
||||
globalEnvironments={globalEnvironments}
|
||||
collection={collection}
|
||||
activeGlobalEnvironmentUid={activeGlobalEnvironmentUid}
|
||||
onClose={handleCloseSettings}
|
||||
/>
|
||||
<GlobalEnvironmentSettings globalEnvironments={globalEnvironments} collection={collection} onClose={handleCloseSettings} />
|
||||
)}
|
||||
|
||||
{showCollectionSettings && <EnvironmentSettings collection={collection} onClose={handleCloseSettings} />}
|
||||
|
||||
@@ -17,7 +17,7 @@ import NTLMAuth from 'components/RequestPane/Auth/NTLMAuth';
|
||||
import WsseAuth from 'components/RequestPane/Auth/WsseAuth';
|
||||
import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth';
|
||||
import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
|
||||
import { humanizeRequestAuthMode, getTreePathFromCollectionToItem } from 'utils/collections/index';
|
||||
import { findItemInCollection, findParentItemInCollection, humanizeRequestAuthMode } from 'utils/collections/index';
|
||||
|
||||
const GrantTypeComponentMap = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -48,7 +48,15 @@ const Auth = ({ collection, folder }) => {
|
||||
let request = get(folder, 'root.request', {});
|
||||
const authMode = get(folder, 'root.request.auth.mode');
|
||||
|
||||
|
||||
const getTreePathFromCollectionToFolder = (collection, _folder) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _folder?.uid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
@@ -61,7 +69,7 @@ const Auth = ({ collection, folder }) => {
|
||||
};
|
||||
|
||||
// Get path from collection to current folder
|
||||
const folderTreePath = getTreePathFromCollectionToItem(collection, folder);
|
||||
const folderTreePath = getTreePathFromCollectionToFolder(collection, folder);
|
||||
|
||||
// Check parent folders to find closest auth configuration
|
||||
// Skip the last item which is the current folder
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash, IconAlertCircle, IconInfoCircle } from '@tabler/icons';
|
||||
import { IconTrash, IconAlertCircle } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import MultiLineEditor from 'components/MultiLineEditor/index';
|
||||
@@ -41,7 +41,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
|
||||
secret: Yup.boolean(),
|
||||
type: Yup.string(),
|
||||
uid: Yup.string(),
|
||||
value: Yup.mixed().nullable()
|
||||
value: Yup.string().trim().nullable()
|
||||
})
|
||||
),
|
||||
onSubmit: (values) => {
|
||||
@@ -136,7 +136,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center" data-testid={`env-var-name-${index}`}>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
@@ -153,31 +153,16 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap items-center">
|
||||
<div className="overflow-hidden grow w-full relative" data-testid={`env-var-value-${index}`}>
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<MultiLineEditor
|
||||
theme={storedTheme}
|
||||
collection={_collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
isSecret={variable.secret}
|
||||
readOnly={typeof variable.value !== 'string'}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
/>
|
||||
</div>
|
||||
{typeof variable.value !== 'string' && (
|
||||
<span className="ml-2 flex items-center">
|
||||
<IconInfoCircle
|
||||
id={`${variable.name}-disabled-info-icon`}
|
||||
className="text-muted"
|
||||
size={16}
|
||||
/>
|
||||
<Tooltip
|
||||
anchorId={`${variable.name}-disabled-info-icon`}
|
||||
content="Non-string values set via scripts are read-only and can only be updated through scripts."
|
||||
place="top"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<input
|
||||
|
||||
@@ -39,7 +39,7 @@ const DefaultTab = ({ setTab }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const EnvironmentSettings = ({ globalEnvironments, collection, activeGlobalEnvironmentUid, onClose }) => {
|
||||
const EnvironmentSettings = ({ globalEnvironments, collection, onClose }) => {
|
||||
const [isModified, setIsModified] = useState(false);
|
||||
const environments = globalEnvironments;
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
||||
@@ -64,7 +64,6 @@ const EnvironmentSettings = ({ globalEnvironments, collection, activeGlobalEnvir
|
||||
<Modal size="lg" title="Global Environments" handleCancel={onClose} hideFooter={true}>
|
||||
<EnvironmentList
|
||||
environments={globalEnvironments}
|
||||
activeEnvironmentUid={activeGlobalEnvironmentUid}
|
||||
selectedEnvironment={selectedEnvironment}
|
||||
setSelectedEnvironment={setSelectedEnvironment}
|
||||
isModified={isModified}
|
||||
|
||||
@@ -90,4 +90,4 @@ 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>
|
||||
);
|
||||
);
|
||||
@@ -1,90 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconChevronDown, IconX } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
|
||||
const InheritableSettingsInput = ({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
description,
|
||||
onKeyDown,
|
||||
isInherited,
|
||||
onDropdownSelect,
|
||||
onValueChange,
|
||||
onCustomValueReset
|
||||
}) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-medium text-gray-900 dark:text-gray-100" htmlFor={id}>
|
||||
{label}
|
||||
</label>
|
||||
{description && (
|
||||
<p className="text-xs text-gray-700 dark:text-gray-400">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
{isInherited ? (
|
||||
<Dropdown
|
||||
icon={(
|
||||
<button
|
||||
type="button"
|
||||
className="px-2 py-1 text-xs rounded-sm outline-none transition-colors duration-100 w-24 h-8 flex items-center justify-between"
|
||||
style={{
|
||||
backgroundColor: theme.modal.input.bg,
|
||||
border: `1px solid ${theme.modal.input.border}`,
|
||||
color: theme.modal.input.text
|
||||
}}
|
||||
>
|
||||
<span>Inherit</span>
|
||||
<IconChevronDown size={12} />
|
||||
</button>
|
||||
)}
|
||||
>
|
||||
<div className="dropdown-item" onClick={() => onDropdownSelect('inherit')}>
|
||||
Inherit
|
||||
</div>
|
||||
<div className="dropdown-item" onClick={() => onDropdownSelect('custom')}>
|
||||
Custom
|
||||
</div>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<input
|
||||
id={id}
|
||||
type="text"
|
||||
className="block px-2 py-1 pr-6 rounded-sm outline-none transition-colors duration-100 w-24 h-8"
|
||||
style={{
|
||||
backgroundColor: theme.modal.input.bg,
|
||||
border: `1px solid ${theme.modal.input.border}`,
|
||||
color: theme.modal.input.text
|
||||
}}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={value}
|
||||
onChange={onValueChange}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCustomValueReset}
|
||||
className="absolute right-1 top-1/2 transform -translate-y-1/2 p-1 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
|
||||
title="Reset to inherit"
|
||||
>
|
||||
<IconX size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InheritableSettingsInput;
|
||||
@@ -78,14 +78,6 @@ const StyledMarkdownBodyWrapper = styled.div`
|
||||
background-color: ${(props) => props.theme.bg};
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
div {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -6,9 +6,6 @@ import { isValidUrl } from 'utils/url/index';
|
||||
|
||||
const Markdown = ({ collectionPath, onDoubleClick, content }) => {
|
||||
const markdownItOptions = {
|
||||
html: true,
|
||||
breaks: true,
|
||||
linkify: true,
|
||||
replaceLink: function (link, env) {
|
||||
return link.replace(/^\./, collectionPath);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (
|
||||
<div className="bruno-modal-header">
|
||||
{customHeader ? customHeader : <>{title ? <div className="bruno-modal-header-title">{title}</div> : null}</>}
|
||||
{handleCancel && !hideClose ? (
|
||||
// TODO: Remove data-test-id and use data-testid instead across the codebase.
|
||||
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null} data-test-id="modal-close-button" data-testid="modal-close-button">
|
||||
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null} data-test-id="modal-close-button">
|
||||
×
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -6,19 +6,6 @@ const StyledWrapper = styled.div`
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
|
||||
&.read-only {
|
||||
.CodeMirror .CodeMirror-lines {
|
||||
cursor: not-allowed !important;
|
||||
user-select: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
-ms-user-select: none !important;
|
||||
}
|
||||
|
||||
.CodeMirror-line {
|
||||
color: ${(props) => props.theme.colors.text.muted} !important;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
background: transparent;
|
||||
height: fit-content;
|
||||
|
||||
@@ -35,7 +35,6 @@ class MultiLineEditor extends Component {
|
||||
brunoVarInfo: {
|
||||
variables
|
||||
},
|
||||
readOnly: this.props.readOnly ? 'nocursor' : false,
|
||||
tabindex: 0,
|
||||
extraKeys: {
|
||||
'Ctrl-Enter': () => {
|
||||
@@ -127,9 +126,6 @@ class MultiLineEditor extends Component {
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
if (this.props.readOnly !== prevProps.readOnly && this.editor) {
|
||||
this.editor.setOption('readOnly', this.props.readOnly ? 'nocursor' : false);
|
||||
}
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
this.cachedValue = String(this.props.value);
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
@@ -186,10 +182,9 @@ class MultiLineEditor extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const wrapperClass = `multi-line-editor grow ${this.props.readOnly ? 'read-only' : ''}`;
|
||||
return (
|
||||
<div className={`flex flex-row justify-between w-full overflow-x-auto ${this.props.className}`}>
|
||||
<StyledWrapper ref={this.editorRef} className={wrapperClass} />
|
||||
<StyledWrapper ref={this.editorRef} className="multi-line-editor grow" />
|
||||
{this.secretEye(this.props.isSecret)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,11 @@ import get from 'lodash/get';
|
||||
|
||||
// Beta features configuration
|
||||
const BETA_FEATURES = [
|
||||
{
|
||||
id: 'grpc',
|
||||
label: 'gRPC Support',
|
||||
description: 'Enable gRPC request support for making gRPC calls to services'
|
||||
},
|
||||
{
|
||||
id: 'nodevm',
|
||||
label: 'Node VM Runtime',
|
||||
@@ -98,6 +103,16 @@ const Beta = ({ close }) => {
|
||||
<label className="block ml-2 select-none font-medium" htmlFor={feature.id}>
|
||||
{feature.label}
|
||||
</label>
|
||||
{feature.id === 'grpc' && (
|
||||
<a
|
||||
href="https://github.com/usebruno/bruno/discussions/5447"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-2 text-xs text-blue-500 hover:text-blue-600 underline"
|
||||
>
|
||||
Share feedback
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="beta-feature-description ml-6 text-xs text-gray-500 dark:text-gray-400">
|
||||
{feature.description}
|
||||
|
||||
@@ -3,7 +3,6 @@ import get from 'lodash/get';
|
||||
import { useFormik } from 'formik';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -36,8 +35,7 @@ const General = ({ close }) => {
|
||||
})
|
||||
.test('isValidTimeout', 'Request Timeout must be equal or greater than 0', (value) => {
|
||||
return value === undefined || Number(value) >= 0;
|
||||
}),
|
||||
defaultCollectionLocation: Yup.string().max(1024)
|
||||
})
|
||||
});
|
||||
|
||||
const formik = useFormik({
|
||||
@@ -52,8 +50,7 @@ const General = ({ close }) => {
|
||||
},
|
||||
timeout: preferences.request.timeout,
|
||||
storeCookies: get(preferences, 'request.storeCookies', true),
|
||||
sendCookies: get(preferences, 'request.sendCookies', true),
|
||||
defaultCollectionLocation: get(preferences, 'general.defaultCollectionLocation', '')
|
||||
sendCookies: get(preferences, 'request.sendCookies', true)
|
||||
},
|
||||
validationSchema: preferencesSchema,
|
||||
onSubmit: async (values) => {
|
||||
@@ -82,9 +79,6 @@ const General = ({ close }) => {
|
||||
timeout: newPreferences.timeout,
|
||||
storeCookies: newPreferences.storeCookies,
|
||||
sendCookies: newPreferences.sendCookies
|
||||
},
|
||||
general: {
|
||||
defaultCollectionLocation: newPreferences.defaultCollectionLocation
|
||||
}
|
||||
}))
|
||||
.then(() => {
|
||||
@@ -105,19 +99,6 @@ const General = ({ close }) => {
|
||||
formik.setFieldValue('customCaCertificate.filePath', null);
|
||||
};
|
||||
|
||||
const browseDefaultLocation = () => {
|
||||
dispatch(browseDirectory())
|
||||
.then((dirPath) => {
|
||||
if (typeof dirPath === 'string') {
|
||||
formik.setFieldValue('defaultCollectionLocation', dirPath);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
formik.setFieldValue('defaultCollectionLocation', '');
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
@@ -250,35 +231,6 @@ const General = ({ close }) => {
|
||||
{formik.touched.timeout && formik.errors.timeout ? (
|
||||
<div className="text-red-500">{formik.errors.timeout}</div>
|
||||
) : null}
|
||||
<div className="flex flex-col mt-6">
|
||||
<label className="block select-none default-collection-location-label" htmlFor="defaultCollectionLocation">
|
||||
Default Collection Location
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="defaultCollectionLocation"
|
||||
className="block textbox mt-2 w-full cursor-pointer default-collection-location-input"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.defaultCollectionLocation || ''}
|
||||
onClick={browseDefaultLocation}
|
||||
placeholder="Click to browse for default location"
|
||||
/>
|
||||
<div className="mt-1">
|
||||
<span
|
||||
className="text-link cursor-pointer hover:underline default-collection-location-browse"
|
||||
onClick={browseDefaultLocation}
|
||||
>
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{formik.touched.defaultCollectionLocation && formik.errors.defaultCollectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.defaultCollectionLocation}</div>
|
||||
) : null}
|
||||
<div className="mt-10">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Save
|
||||
|
||||
@@ -9,7 +9,6 @@ import WsseAuth from './WsseAuth';
|
||||
import NTLMAuth from './NTLMAuth';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import ApiKeyAuth from './ApiKeyAuth';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -28,7 +27,6 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
};
|
||||
|
||||
const Auth = ({ 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);
|
||||
|
||||
@@ -39,7 +37,7 @@ const Auth = ({ item, collection }) => {
|
||||
|
||||
// Save function for request level
|
||||
const save = () => {
|
||||
return dispatch(saveRequest(item.uid, collection.uid));
|
||||
return saveRequest(item.uid, collection.uid);
|
||||
};
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
|
||||
@@ -17,218 +17,229 @@ import toast from 'react-hot-toast'
|
||||
import { getAbsoluteFilePath } from 'utils/common/path';
|
||||
|
||||
const SingleGrpcMessage = ({ 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));
|
||||
|
||||
// Access gRPC method metadata from local storage
|
||||
const [reflectionCache] = useLocalStorage('bruno.grpc.reflectionCache', {});
|
||||
const [protofileCache] = useLocalStorage('bruno.grpc.protofileCache', {});
|
||||
|
||||
const canClientStream = methodType === 'client-streaming' || methodType === 'bidi-streaming';
|
||||
|
||||
const { name, content } = message;
|
||||
|
||||
const onEdit = (value) => {
|
||||
const currentMessages = [...(body.grpc || [])];
|
||||
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));
|
||||
|
||||
currentMessages[index] = {
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
content: value
|
||||
// Access gRPC method metadata from local storage
|
||||
const [reflectionCache] = useLocalStorage('bruno.grpc.reflectionCache', {});
|
||||
const [protofileCache] = useLocalStorage('bruno.grpc.protofileCache', {});
|
||||
|
||||
const canClientStream = methodType === 'client-streaming' || methodType === 'bidi-streaming';
|
||||
|
||||
const { name, content } = message;
|
||||
|
||||
const onEdit = (value) => {
|
||||
const currentMessages = [...(body.grpc || [])];
|
||||
|
||||
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 sendGrpcMessage(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;
|
||||
}
|
||||
|
||||
// Get the URL and protoPath to determine which cache to use
|
||||
const url = item.draft?.request?.url || item.request?.url;
|
||||
const protoPath = item.draft?.request?.protoPath || item.request?.protoPath;
|
||||
|
||||
// Find the method metadata from the appropriate cache
|
||||
let methodMetadata = null;
|
||||
if (protoPath) {
|
||||
// Use protofile cache if protoPath is available
|
||||
const absolutePath = getAbsoluteFilePath(protoPath, collection.pathname);
|
||||
const cachedMethods = protofileCache[absolutePath];
|
||||
if (cachedMethods) {
|
||||
methodMetadata = cachedMethods.find(method => method.path === methodPath);
|
||||
}
|
||||
} else if (url) {
|
||||
// Use reflection cache if no protoPath (reflection mode)
|
||||
const cachedMethods = reflectionCache[url];
|
||||
if (cachedMethods) {
|
||||
methodMetadata = cachedMethods.find(method => method.path === methodPath);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await generateGrpcSampleMessage(
|
||||
methodPath,
|
||||
content,
|
||||
{
|
||||
arraySize: 2,
|
||||
methodMetadata // Pass the method metadata to the function
|
||||
}
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
const currentMessages = [...(body.grpc || [])];
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
const onDeleteMessage = () => {
|
||||
const currentMessages = [...(body.grpc || [])];
|
||||
|
||||
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.grpc || [])];
|
||||
currentMessages[index] = {
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
content: prettyBodyJson
|
||||
};
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
};
|
||||
|
||||
const onSend = async () => {
|
||||
try {
|
||||
await sendGrpcMessage(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;
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
toastError(new Error('Unable to prettify. Invalid JSON format.'));
|
||||
}
|
||||
};
|
||||
|
||||
// Get the URL and protoPath to determine which cache to use
|
||||
const url = item.draft?.request?.url || item.request?.url;
|
||||
const protoPath = item.draft?.request?.protoPath || item.request?.protoPath;
|
||||
const getContainerHeight = (canClientSendMultipleMessages && body.grpc.length > 1) ? `${isCollapsed ? "" : "h-80"}` : "h-full"
|
||||
|
||||
// Find the method metadata from the appropriate cache
|
||||
let methodMetadata = null;
|
||||
if (protoPath) {
|
||||
// Use protofile cache if protoPath is available
|
||||
const absolutePath = getAbsoluteFilePath(collection.pathname, protoPath);
|
||||
const cachedMethods = protofileCache[absolutePath];
|
||||
if (cachedMethods) {
|
||||
methodMetadata = cachedMethods.find((method) => method.path === methodPath);
|
||||
}
|
||||
} else if (url) {
|
||||
// Use reflection cache if no protoPath (reflection mode)
|
||||
const cachedMethods = reflectionCache[url];
|
||||
if (cachedMethods) {
|
||||
methodMetadata = cachedMethods.find((method) => method.path === methodPath);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await generateGrpcSampleMessage(methodPath,
|
||||
content,
|
||||
{
|
||||
arraySize: 2,
|
||||
methodMetadata // Pass the method metadata to the function
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
const currentMessages = [...(body.grpc || [])];
|
||||
|
||||
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.grpc || [])];
|
||||
|
||||
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.grpc || [])];
|
||||
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.grpc.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="grpc-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 gRPC 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}
|
||||
return (
|
||||
<div className={`flex flex-col mb-3 border border-neutral-200 dark:border-neutral-800 rounded-md overflow-hidden ${getContainerHeight} relative`}>
|
||||
<div
|
||||
className="grpc-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"
|
||||
>
|
||||
<IconTrash size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
<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 gRPC 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.grpc.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>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className={`flex ${body.grpc.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 GrpcBody = ({ item, collection, handleRun }) => {
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
@@ -237,10 +248,10 @@ const GrpcBody = ({ item, collection, handleRun }) => {
|
||||
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 = methodType === 'client-streaming' || methodType === 'bidi-streaming';
|
||||
|
||||
|
||||
// Auto-scroll to the latest message when messages are added
|
||||
useEffect(() => {
|
||||
if (messagesContainerRef.current && body?.grpc?.length > 0) {
|
||||
@@ -248,7 +259,7 @@ const GrpcBody = ({ item, collection, handleRun }) => {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}, [body?.grpc?.length]);
|
||||
|
||||
|
||||
const toggleMessageCollapse = (index) => {
|
||||
setCollapsedMessages(prev => {
|
||||
if (prev.includes(index)) {
|
||||
@@ -258,31 +269,34 @@ const GrpcBody = ({ item, collection, handleRun }) => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const addNewMessage = () => {
|
||||
const currentMessages = Array.isArray(body.grpc)
|
||||
? [...body.grpc]
|
||||
: [];
|
||||
|
||||
const currentMessages = Array.isArray(body.grpc)
|
||||
? [...body.grpc]
|
||||
: [];
|
||||
|
||||
currentMessages.push({
|
||||
name: `message ${currentMessages.length + 1}`,
|
||||
content: '{}'
|
||||
});
|
||||
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
if (!body?.grpc || !Array.isArray(body.grpc)) {
|
||||
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 gRPC messages available</p>
|
||||
<ToolHint text="Add the first message to your gRPC request" toolhintId="add-first-msg">
|
||||
<button
|
||||
<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"
|
||||
>
|
||||
@@ -294,21 +308,21 @@ const GrpcBody = ({ item, collection, handleRun }) => {
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<StyledWrapper isVerticalLayout={isVerticalLayout}>
|
||||
<div
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
id="grpc-messages-container"
|
||||
className={`flex-1 ${body.grpc.length === 1 || !canClientSendMultipleMessages ? 'h-full' : 'overflow-y-auto'} ${canClientSendMultipleMessages && 'pb-16'}`}
|
||||
id="grpc-messages-container"
|
||||
className={`flex-1 ${body.grpc.length === 1 || !canClientSendMultipleMessages ? "h-full" : "overflow-y-auto"} ${canClientSendMultipleMessages && "pb-16"}`}
|
||||
>
|
||||
{body.grpc
|
||||
.filter((_, index) => canClientSendMultipleMessages || index === 0)
|
||||
.map((message, index) => (
|
||||
<SingleGrpcMessage
|
||||
<SingleGrpcMessage
|
||||
key={index}
|
||||
message={message}
|
||||
item={item}
|
||||
message={message}
|
||||
item={item}
|
||||
collection={collection}
|
||||
index={index}
|
||||
methodType={methodType}
|
||||
@@ -317,13 +331,13 @@ const GrpcBody = ({ item, collection, handleRun }) => {
|
||||
handleRun={handleRun}
|
||||
canClientSendMultipleMessages={canClientSendMultipleMessages}
|
||||
/>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
{canClientSendMultipleMessages && (
|
||||
<div className="add-message-btn-container">
|
||||
<ToolHint text="Add a new gRPC message to the request" toolhintId="add-msg-fixed">
|
||||
<button
|
||||
<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"
|
||||
>
|
||||
@@ -337,4 +351,4 @@ const GrpcBody = ({ item, collection, handleRun }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default GrpcBody;
|
||||
export default GrpcBody;
|
||||
@@ -1,64 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { IconCheck, IconCopy } from '@tabler/icons';
|
||||
import toast from 'react-hot-toast';
|
||||
import get from 'lodash/get';
|
||||
import Modal from 'components/Modal/index';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
|
||||
const GrpcurlModal = ({ isOpen, onClose, command }) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(command);
|
||||
setCopied(true);
|
||||
toast.success('Command copied to clipboard');
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (error) {
|
||||
toast.error('Failed to copy command');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
handleCancel={onClose}
|
||||
title={(
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Generate gRPCurl Command</span>
|
||||
</div>
|
||||
)}
|
||||
size="lg"
|
||||
hideFooter={true}
|
||||
>
|
||||
<div>
|
||||
<div className="flex w-full min-h-[400px]">
|
||||
<div className="flex-grow relative">
|
||||
<div className="absolute top-2 right-2 z-10">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="btn btn-sm btn-secondary flex items-center gap-2"
|
||||
>
|
||||
{copied ? <IconCheck size={20} /> : <IconCopy size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
<CodeEditor
|
||||
value={command}
|
||||
theme={displayedTheme}
|
||||
readOnly={true}
|
||||
mode="shell"
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrpcurlModal;
|
||||
@@ -1,131 +0,0 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import { IconChevronDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown/index';
|
||||
import {
|
||||
IconGrpcUnary,
|
||||
IconGrpcClientStreaming,
|
||||
IconGrpcServerStreaming,
|
||||
IconGrpcBidiStreaming
|
||||
} from 'components/Icons/Grpc';
|
||||
|
||||
const MethodDropdown = ({
|
||||
grpcMethods,
|
||||
selectedGrpcMethod,
|
||||
onMethodSelect,
|
||||
onMethodDropdownCreate
|
||||
}) => {
|
||||
const groupMethodsByService = (methods) => {
|
||||
if (!methods || !methods.length) return {};
|
||||
|
||||
const groupedMethods = {};
|
||||
|
||||
methods.forEach((method) => {
|
||||
const pathWithoutLeadingSlash = method.path.startsWith('/') ? method.path.slice(1) : method.path;
|
||||
const parts = pathWithoutLeadingSlash.split('/');
|
||||
const serviceName = parts[0] || 'Default';
|
||||
const methodName = parts[1] || method.path;
|
||||
|
||||
const enhancedMethod = {
|
||||
...method,
|
||||
serviceName,
|
||||
methodName
|
||||
};
|
||||
|
||||
if (!groupedMethods[serviceName]) {
|
||||
groupedMethods[serviceName] = [];
|
||||
}
|
||||
|
||||
groupedMethods[serviceName].push(enhancedMethod);
|
||||
});
|
||||
|
||||
return groupedMethods;
|
||||
};
|
||||
|
||||
const getIconForMethodType = (type) => {
|
||||
switch (type) {
|
||||
case 'unary':
|
||||
return <IconGrpcUnary size={20} strokeWidth={2} />;
|
||||
case 'client-streaming':
|
||||
return <IconGrpcClientStreaming size={20} strokeWidth={2} />;
|
||||
case 'server-streaming':
|
||||
return <IconGrpcServerStreaming size={20} strokeWidth={2} />;
|
||||
case 'bidi-streaming':
|
||||
return <IconGrpcBidiStreaming size={20} strokeWidth={2} />;
|
||||
default:
|
||||
return <IconGrpcUnary size={20} strokeWidth={2} />;
|
||||
}
|
||||
};
|
||||
|
||||
const MethodsDropdownIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center ml-2 cursor-pointer select-none">
|
||||
{selectedGrpcMethod && <div className="mr-2">{getIconForMethodType(selectedGrpcMethod.type)}</div>}
|
||||
<span className="text-xs">
|
||||
{selectedGrpcMethod ? (
|
||||
<span className="dark:text-neutral-300 text-neutral-700 text-nowrap">
|
||||
{selectedGrpcMethod.path.split('.').at(-1) || selectedGrpcMethod.path}
|
||||
</span>
|
||||
) : (
|
||||
<span className="dark:text-neutral-300 text-neutral-700 text-nowrap">Select Method </span>
|
||||
)}
|
||||
</span>
|
||||
<IconChevronDown className="caret ml-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleGrpcMethodSelect = (method) => {
|
||||
const methodType = method.type;
|
||||
onMethodSelect({ path: method.path, type: methodType });
|
||||
};
|
||||
|
||||
if (!grpcMethods || grpcMethods.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center h-full mr-2" data-testid="grpc-methods-dropdown">
|
||||
<Dropdown onCreate={onMethodDropdownCreate} icon={<MethodsDropdownIcon />} placement="bottom-end" style={{ maxWidth: 'unset' }}>
|
||||
<div className="max-h-96 overflow-y-auto max-w-96 min-w-60" data-testid="grpc-methods-list">
|
||||
{Object.entries(groupMethodsByService(grpcMethods)).map(([serviceName, methods], serviceIndex) => (
|
||||
<div key={serviceIndex} className="service-group mb-2">
|
||||
<div className="service-header px-3 py-1 bg-neutral-100 dark:bg-neutral-800 text-sm font-medium truncate sticky top-0 z-10">
|
||||
{serviceName || 'Default Service'}
|
||||
</div>
|
||||
<div className="service-methods">
|
||||
{methods.map((method, methodIndex) => (
|
||||
<div
|
||||
key={`${serviceIndex}-${methodIndex}`}
|
||||
className={`py-2 px-3 w-full border-l-2 transition-all duration-200 relative group ${
|
||||
selectedGrpcMethod && selectedGrpcMethod.path === method.path
|
||||
? 'border-yellow-500 bg-yellow-500/20 dark:bg-yellow-900/20'
|
||||
: 'border-transparent hover:bg-black/5 dark:hover:bg-white/5'
|
||||
}`}
|
||||
onClick={() => handleGrpcMethodSelect(method)}
|
||||
data-testid="grpc-method-item"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="text-xs mr-3 text-gray-500">
|
||||
{getIconForMethodType(method.type)}
|
||||
</div>
|
||||
<div className="flex flex-col flex-1">
|
||||
<div className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{method.methodName}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{method.type}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MethodDropdown;
|
||||
@@ -1,217 +0,0 @@
|
||||
import React, { forwardRef, useState } from 'react';
|
||||
import { IconFile, IconChevronDown } from '@tabler/icons';
|
||||
import { getBasename } from 'utils/common/path';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateRequestProtoPath } from 'providers/ReduxStore/slices/collections';
|
||||
import { openCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import Dropdown from 'components/Dropdown/index';
|
||||
import ToggleSwitch from 'components/ToggleSwitch/index';
|
||||
import { TabNavigation, ProtoFilesTab, ImportPathsTab } from '../Tabs';
|
||||
import useProtoFileManagement from 'hooks/useProtoFileManagement/index';
|
||||
|
||||
const ProtoFileDropdown = ({
|
||||
collection,
|
||||
item,
|
||||
isReflectionMode,
|
||||
protoFilePath,
|
||||
showProtoDropdown,
|
||||
setShowProtoDropdown,
|
||||
onProtoDropdownCreate,
|
||||
onReflectionModeToggle,
|
||||
onProtoFileLoad
|
||||
}) => {
|
||||
const { theme } = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const [activeTab, setActiveTab] = useState('protofiles'); // 'protofiles' or 'importpaths'
|
||||
const protoFileManagement = useProtoFileManagement(collection, protoFilePath);
|
||||
const invalidProtoFiles = protoFileManagement.protoFiles.filter((file) => !file.exists);
|
||||
const invalidImportPaths = protoFileManagement.importPaths.filter((path) => !path.exists);
|
||||
|
||||
const handleSelectProtoFile = async (e) => {
|
||||
e.stopPropagation();
|
||||
const { success, filePath, error } = await protoFileManagement.browseForProtoFile();
|
||||
if (!success) {
|
||||
if (error) {
|
||||
toast.error(`Failed to browse for proto file: ${error.message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { success: addSuccess, relativePath, alreadyExists, error: addError } = await protoFileManagement.addProtoFileToCollection(filePath);
|
||||
if (!addSuccess) {
|
||||
if (addError) {
|
||||
toast.error(`Failed to add proto file: ${addError.message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (alreadyExists) {
|
||||
toast.error('Proto file already exists in collection settings');
|
||||
} else {
|
||||
toast.success('Added proto file to collection');
|
||||
}
|
||||
|
||||
dispatch(updateRequestProtoPath({
|
||||
protoPath: relativePath,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
|
||||
setShowProtoDropdown(false);
|
||||
|
||||
onProtoFileLoad(relativePath);
|
||||
};
|
||||
|
||||
const handleSelectCollectionProtoFile = (protoFile) => {
|
||||
if (!protoFile || !protoFile.exists) {
|
||||
toast.error('Proto file not found');
|
||||
return;
|
||||
}
|
||||
|
||||
setShowProtoDropdown(false);
|
||||
|
||||
dispatch(updateRequestProtoPath({
|
||||
protoPath: protoFile.path,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
|
||||
onProtoFileLoad(protoFile.path);
|
||||
};
|
||||
|
||||
const handleBrowseImportPath = async (e) => {
|
||||
e.stopPropagation();
|
||||
const { success, directoryPath, error } = await protoFileManagement.browseForImportDirectory();
|
||||
if (!success) {
|
||||
if (error) {
|
||||
toast.error(`Failed to browse for import directory: ${error.message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { success: addSuccess, error: addError } = await protoFileManagement.addImportPathToCollection(directoryPath);
|
||||
if (!addSuccess) {
|
||||
if (addError) {
|
||||
toast.error(`Failed to add import path: ${addError.message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success('Added import path to collection');
|
||||
};
|
||||
|
||||
const handleToggleImportPath = async (index) => {
|
||||
const { success, enabled, error } = await protoFileManagement.toggleImportPath(index);
|
||||
if (!success) {
|
||||
if (error) {
|
||||
toast.error(`Failed to toggle import path: ${error.message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(`Import path ${enabled ? 'enabled' : 'disabled'}`);
|
||||
};
|
||||
|
||||
const handleOpenCollectionProtobufSettings = (e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(openCollectionSettings(collection.uid, 'protobuf'));
|
||||
};
|
||||
|
||||
const ProtoFileDropdownIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center cursor-pointer select-none" onClick={() => setShowProtoDropdown((prev) => !prev)} data-testid="grpc-proto-file-dropdown-icon">
|
||||
{isReflectionMode ? (<></>
|
||||
) : (
|
||||
<IconFile size={20} strokeWidth={1.5} className="mr-1 text-neutral-400" />
|
||||
)}
|
||||
<span className="text-xs dark:text-neutral-300 text-neutral-700 text-nowrap">
|
||||
{isReflectionMode ? 'Using Reflection' : (protoFilePath ? getBasename(collection.pathname, protoFilePath) : 'Select Proto File')}
|
||||
</span>
|
||||
<IconChevronDown className="caret ml-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="proto-file-dropdown">
|
||||
<Dropdown
|
||||
onCreate={onProtoDropdownCreate}
|
||||
icon={<ProtoFileDropdownIcon />}
|
||||
placement="bottom-end"
|
||||
visible={showProtoDropdown}
|
||||
onClickOutside={() => setShowProtoDropdown(false)}
|
||||
data-testid="grpc-proto-file-dropdown"
|
||||
>
|
||||
<div className="max-h-fit overflow-y-auto w-[30rem]">
|
||||
<div className="px-3 py-2 border-b border-neutral-200 dark:border-neutral-700" data-testid="grpc-mode-toggle">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Mode</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-xs ${!isReflectionMode ? 'font-medium' : 'text-neutral-500'}`} style={{ color: !isReflectionMode ? theme.colors.text.yellow : undefined }}>
|
||||
Proto File
|
||||
</span>
|
||||
<ToggleSwitch
|
||||
isOn={isReflectionMode}
|
||||
handleToggle={onReflectionModeToggle}
|
||||
size="2xs"
|
||||
activeColor={theme.colors.text.yellow}
|
||||
/>
|
||||
<span className={`text-xs ${isReflectionMode ? 'font-medium' : 'text-neutral-500'}`} style={{ color: isReflectionMode ? theme.colors.text.yellow : undefined }}>
|
||||
Reflection
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isReflectionMode && (
|
||||
<TabNavigation
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
collectionProtoFiles={protoFileManagement.protoFiles}
|
||||
collectionImportPaths={protoFileManagement.importPaths}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isReflectionMode && (
|
||||
<>
|
||||
{activeTab === 'protofiles' && (
|
||||
<ProtoFilesTab
|
||||
collectionProtoFiles={protoFileManagement.protoFiles}
|
||||
invalidProtoFiles={invalidProtoFiles}
|
||||
protoFilePath={protoFilePath}
|
||||
collection={collection}
|
||||
onSelectCollectionProtoFile={handleSelectCollectionProtoFile}
|
||||
onOpenCollectionProtobufSettings={handleOpenCollectionProtobufSettings}
|
||||
onSelectProtoFile={handleSelectProtoFile}
|
||||
setShowProtoDropdown={setShowProtoDropdown}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'importpaths' && (
|
||||
<ImportPathsTab
|
||||
collectionImportPaths={protoFileManagement.importPaths}
|
||||
invalidImportPaths={invalidImportPaths}
|
||||
onOpenCollectionProtobufSettings={handleOpenCollectionProtobufSettings}
|
||||
onBrowseImportPath={handleBrowseImportPath}
|
||||
onToggleImportPath={handleToggleImportPath}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isReflectionMode && (
|
||||
<div className="px-3 py-2">
|
||||
<div className="text-sm text-neutral-600 dark:text-neutral-400 mb-2">
|
||||
Using server reflection to discover gRPC methods.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProtoFileDropdown;
|
||||
@@ -1,154 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.content-wrapper {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.header-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
font-size: 0.75rem;
|
||||
color: ${(props) => props.theme.grpc.importPaths.header.text};
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
color: ${(props) => props.theme.grpc.importPaths.header.button.color};
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.grpc.importPaths.header.button.hoverColor};
|
||||
}
|
||||
}
|
||||
|
||||
.error-wrapper {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background-color: ${(props) => props.theme.grpc.importPaths.error.bg};
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: ${(props) => props.theme.grpc.importPaths.error.text};
|
||||
}
|
||||
|
||||
.error-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error-link {
|
||||
color: ${(props) => props.theme.grpc.importPaths.error.link.color};
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
margin-left: 0.25rem;
|
||||
font-size: inherit;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.grpc.importPaths.error.link.hoverColor};
|
||||
}
|
||||
}
|
||||
|
||||
.items-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
max-height: 15rem;
|
||||
overflow: auto;
|
||||
max-width: 30rem;
|
||||
}
|
||||
|
||||
.item-wrapper {
|
||||
padding: 0.5rem 0.75rem;
|
||||
opacity: ${(props) => props.theme.grpc.importPaths.item.invalid.opacity};
|
||||
|
||||
&.valid {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-right: 0.5rem;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.grpc.importPaths.item.checkbox.color};
|
||||
}
|
||||
|
||||
.item-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.invalid-icon {
|
||||
color: ${(props) => props.theme.grpc.importPaths.item.invalid.text};
|
||||
font-size: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.empty-wrapper {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: ${(props) => props.theme.grpc.importPaths.empty.text};
|
||||
font-size: 0.875rem;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.browse-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: ${(props) => props.theme.grpc.importPaths.button.bg};
|
||||
color: ${(props) => props.theme.grpc.importPaths.button.color};
|
||||
border: 1px solid ${(props) => props.theme.grpc.importPaths.button.border};
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: ${(props) => props.theme.grpc.importPaths.button.hoverBorder};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,102 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconFolder, IconSettings, IconAlertCircle, IconFileImport } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ImportPathsTab = ({
|
||||
collectionImportPaths,
|
||||
invalidImportPaths,
|
||||
onOpenCollectionProtobufSettings,
|
||||
onBrowseImportPath,
|
||||
onToggleImportPath
|
||||
}) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{collectionImportPaths && collectionImportPaths.length > 0 && (
|
||||
<div className="content-wrapper">
|
||||
<div className="header-wrapper">
|
||||
<div className="header-text">From Collection Settings</div>
|
||||
<button
|
||||
onClick={onOpenCollectionProtobufSettings}
|
||||
className="settings-button"
|
||||
>
|
||||
<IconSettings size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{invalidImportPaths.length > 0 && (
|
||||
<div className="error-wrapper">
|
||||
<p className="error-text">
|
||||
<IconAlertCircle size={16} strokeWidth={1.5} style={{ marginRight: '0.25rem' }} />
|
||||
Some import paths could not be found.
|
||||
{' '}
|
||||
<button
|
||||
onClick={onOpenCollectionProtobufSettings}
|
||||
className="error-link"
|
||||
>
|
||||
Manage import paths
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="items-container">
|
||||
{collectionImportPaths.map((importPath, index) => {
|
||||
const isInvalid = !importPath.exists;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`collection-import-${index}`}
|
||||
className={`item-wrapper ${!isInvalid ? 'valid' : ''}`}
|
||||
>
|
||||
<div className="item-content">
|
||||
<div className="item-left">
|
||||
<div className="checkbox-wrapper">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={importPath.enabled}
|
||||
disabled={isInvalid}
|
||||
onChange={() => onToggleImportPath(index)}
|
||||
className="checkbox"
|
||||
title={importPath.enabled ? 'Import path enabled' : 'Import path disabled'}
|
||||
/>
|
||||
</div>
|
||||
<IconFolder size={20} strokeWidth={1.5} style={{ marginRight: '0.5rem', color: 'inherit' }} />
|
||||
<div className="item-text">
|
||||
{importPath.path}
|
||||
{isInvalid && (
|
||||
<span className="invalid-icon">
|
||||
<IconAlertCircle size={16} strokeWidth={1.5} style={{ margin: '0 0.25rem' }} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!collectionImportPaths || collectionImportPaths.length === 0) && (
|
||||
<div className="empty-wrapper">
|
||||
<div className="empty-text">
|
||||
No import paths configured in collection settings
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="button-wrapper">
|
||||
<button
|
||||
className="browse-button"
|
||||
onClick={onBrowseImportPath}
|
||||
>
|
||||
<IconFileImport size={16} strokeWidth={1.5} style={{ marginRight: '0.25rem' }} />
|
||||
Browse for Import Path
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportPathsTab;
|
||||
@@ -1,172 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.content-wrapper {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.header-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
font-size: 0.75rem;
|
||||
color: ${(props) => props.theme.grpc.protoFiles.header.text};
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
color: ${(props) => props.theme.grpc.protoFiles.header.button.color};
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.grpc.protoFiles.header.button.hoverColor};
|
||||
}
|
||||
}
|
||||
|
||||
.error-wrapper {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background-color: ${(props) => props.theme.grpc.protoFiles.error.bg};
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: ${(props) => props.theme.grpc.protoFiles.error.text};
|
||||
}
|
||||
|
||||
.error-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error-link {
|
||||
color: ${(props) => props.theme.grpc.protoFiles.error.link.color};
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
margin-left: 0.25rem;
|
||||
font-size: inherit;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.grpc.protoFiles.error.link.hoverColor};
|
||||
}
|
||||
}
|
||||
|
||||
.items-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
max-height: 15rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.item-wrapper {
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
border-left: 2px solid transparent;
|
||||
background-color: ${(props) => props.theme.grpc.protoFiles.item.bg};
|
||||
transition: all 0.2s ease;
|
||||
opacity: ${(props) => props.theme.grpc.protoFiles.item.invalid.opacity};
|
||||
|
||||
&.valid {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-left-color: ${(props) => props.theme.grpc.protoFiles.item.selected.border};
|
||||
background-color: ${(props) => props.theme.grpc.protoFiles.item.selected.bg};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.grpc.protoFiles.item.hoverBg};
|
||||
|
||||
&.selected {
|
||||
background-color: ${(props) => props.theme.grpc.protoFiles.item.selected.bg};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
margin-right: 0.75rem;
|
||||
color: ${(props) => props.theme.grpc.protoFiles.item.icon};
|
||||
}
|
||||
|
||||
.item-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 0.875rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${(props) => props.theme.grpc.protoFiles.item.text};
|
||||
}
|
||||
|
||||
.item-path {
|
||||
font-size: 0.75rem;
|
||||
color: ${(props) => props.theme.grpc.protoFiles.item.secondaryText};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 12.5rem;
|
||||
}
|
||||
|
||||
.invalid-icon {
|
||||
color: ${(props) => props.theme.grpc.protoFiles.item.invalid.text};
|
||||
font-size: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.empty-wrapper {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: ${(props) => props.theme.grpc.protoFiles.empty.text};
|
||||
font-size: 0.875rem;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.browse-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: ${(props) => props.theme.grpc.protoFiles.button.bg};
|
||||
color: ${(props) => props.theme.grpc.protoFiles.button.color};
|
||||
border: 1px solid ${(props) => props.theme.grpc.protoFiles.button.border};
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: ${(props) => props.theme.grpc.protoFiles.button.hoverBorder};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,106 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconFile, IconSettings, IconAlertCircle } from '@tabler/icons';
|
||||
import { getBasename } from 'utils/common/path';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ProtoFilesTab = ({
|
||||
collectionProtoFiles,
|
||||
invalidProtoFiles,
|
||||
protoFilePath,
|
||||
collection,
|
||||
onSelectCollectionProtoFile,
|
||||
onOpenCollectionProtobufSettings,
|
||||
onSelectProtoFile
|
||||
}) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{collectionProtoFiles && collectionProtoFiles.length > 0 && (
|
||||
<div className="content-wrapper">
|
||||
<div className="header-wrapper">
|
||||
<div className="header-text">From Collection Settings</div>
|
||||
<button
|
||||
onClick={onOpenCollectionProtobufSettings}
|
||||
className="settings-button"
|
||||
>
|
||||
<IconSettings size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{invalidProtoFiles.length > 0 && (
|
||||
<div className="error-wrapper">
|
||||
<p className="error-text">
|
||||
<IconAlertCircle size={16} strokeWidth={1.5} style={{ marginRight: '0.25rem' }} />
|
||||
Some proto files could not be found.
|
||||
{' '}
|
||||
<button
|
||||
onClick={onOpenCollectionProtobufSettings}
|
||||
className="error-link"
|
||||
>
|
||||
Manage proto files
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="items-container">
|
||||
{collectionProtoFiles.map((protoFile, index) => {
|
||||
const isSelected = protoFilePath === protoFile.path;
|
||||
const isInvalid = !protoFile.exists;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`collection-proto-${index}`}
|
||||
className={`item-wrapper ${!isInvalid ? 'valid' : ''} ${isSelected ? 'selected' : ''}`}
|
||||
onClick={() => {
|
||||
if (!isInvalid) {
|
||||
onSelectCollectionProtoFile(protoFile);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="item-content">
|
||||
<div className="item-icon">
|
||||
<IconFile size={20} strokeWidth={1.5} />
|
||||
</div>
|
||||
<div className="item-details">
|
||||
<div className="item-title">
|
||||
{getBasename(collection.pathname, protoFile.path)}
|
||||
{isInvalid && (
|
||||
<span className="invalid-icon">
|
||||
<IconAlertCircle size={14} strokeWidth={1.5} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="item-path">
|
||||
{protoFile.path}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!collectionProtoFiles || collectionProtoFiles.length === 0) && (
|
||||
<div className="empty-wrapper">
|
||||
<div className="empty-text">
|
||||
No proto files configured in collection settings
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="button-wrapper">
|
||||
<button
|
||||
className="browse-button"
|
||||
onClick={onSelectProtoFile}
|
||||
>
|
||||
<IconFile size={16} strokeWidth={1.5} style={{ marginRight: '0.25rem' }} />
|
||||
Browse for Proto File
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProtoFilesTab;
|
||||
@@ -1,23 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.tab-container {
|
||||
background-color: ${(props) => props.theme.grpc.tabNav.container.bg};
|
||||
}
|
||||
.tab-button {
|
||||
background-color: ${(props) => props.theme.grpc.tabNav.button.inactive.bg};
|
||||
color: ${(props) => props.theme.grpc.tabNav.button.inactive.color};
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.grpc.tabNav.button.inactive.hover};
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: ${(props) => props.theme.grpc.tabNav.button.active.bg};
|
||||
color: ${(props) => props.theme.grpc.tabNav.button.active.color};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const TabNavigation = ({ activeTab, onTabChange, collectionProtoFiles, collectionImportPaths }) => {
|
||||
return (
|
||||
<StyledWrapper className="px-3 py-2 border-b border-neutral-200 dark:border-neutral-700">
|
||||
<div className="tab-container flex space-x-1 rounded-lg p-1">
|
||||
<button
|
||||
className={`flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-colors tab-button ${activeTab === 'protofiles' ? 'active' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTabChange('protofiles');
|
||||
}}
|
||||
>
|
||||
Proto Files (
|
||||
{collectionProtoFiles?.length || 0}
|
||||
)
|
||||
</button>
|
||||
<button
|
||||
className={`flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-colors tab-button ${activeTab === 'importpaths' ? 'active' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTabChange('importpaths');
|
||||
}}
|
||||
>
|
||||
Import Paths (
|
||||
{collectionImportPaths?.length || 0}
|
||||
)
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabNavigation;
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default as TabNavigation } from './TabNavigation';
|
||||
export { default as ProtoFilesTab } from './ProtoFilesTab';
|
||||
export { default as ImportPathsTab } from './ImportPathsTab';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ import { humanizeRequestBodyMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import { prettifyJSON } from 'utils/common';
|
||||
import { format, applyEdits } from 'jsonc-parser';
|
||||
import xmlFormat from 'xml-formatter';
|
||||
|
||||
const RequestBodyMode = ({ item, collection }) => {
|
||||
@@ -39,7 +39,8 @@ const RequestBodyMode = ({ item, collection }) => {
|
||||
const onPrettify = () => {
|
||||
if (body?.json && bodyMode === 'json') {
|
||||
try {
|
||||
const prettyBodyJson = prettifyJSON(body.json);
|
||||
const edits = format(body.json, undefined, { tabSize: 2, insertSpaces: true });
|
||||
const prettyBodyJson = applyEdits(body.json, edits);
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
content: prettyBodyJson,
|
||||
|
||||
@@ -6,8 +6,7 @@ const ToggleSelector = ({
|
||||
label,
|
||||
description,
|
||||
disabled = false,
|
||||
size = 'small', // 'small', 'medium', 'large'
|
||||
'data-testid': dataTestId
|
||||
size = 'small' // 'small', 'medium', 'large'
|
||||
}) => {
|
||||
const sizeClasses = {
|
||||
small: {
|
||||
@@ -30,24 +29,13 @@ const ToggleSelector = ({
|
||||
const currentSize = sizeClasses[size];
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-medium text-gray-900 dark:text-gray-100">
|
||||
{label}
|
||||
</label>
|
||||
{description && (
|
||||
<p className="text-xs text-gray-700 dark:text-gray-400">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onChange}
|
||||
disabled={disabled}
|
||||
data-testid={dataTestId}
|
||||
className={`
|
||||
relative inline-flex ${currentSize.container} flex-shrink-0 items-center rounded-full transition-colors
|
||||
relative inline-flex ${currentSize.container} mx-1 items-center rounded-full transition-colors
|
||||
focus:outline-none focus:ring-1 focus:ring-offset-1
|
||||
${disabled
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
@@ -69,6 +57,16 @@ const ToggleSelector = ({
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-medium text-gray-900 dark:text-gray-100">
|
||||
{label}
|
||||
</label>
|
||||
{description && (
|
||||
<p className="text-xs text-gray-700 dark:text-gray-400">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,156 +1,47 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import get from 'lodash/get';
|
||||
import { IconTag } from '@tabler/icons';
|
||||
import ToggleSelector from 'components/RequestPane/Settings/ToggleSelector';
|
||||
import SettingsInput from 'components/SettingsInput';
|
||||
import InheritableSettingsInput from 'components/InheritableSettingsInput';
|
||||
import { updateItemSettings } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Tags from './Tags/index';
|
||||
|
||||
// Default settings configuration
|
||||
const DEFAULT_SETTINGS = {
|
||||
encodeUrl: false,
|
||||
followRedirects: true,
|
||||
maxRedirects: 5,
|
||||
timeout: 'inherit'
|
||||
};
|
||||
|
||||
const Settings = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Get current settings with defaults applied
|
||||
// get the length of active params, headers, asserts and vars as well as the contents of the body, tests and script
|
||||
const getPropertyFromDraftOrRequest = (propertyKey) =>
|
||||
item.draft ? get(item, `draft.${propertyKey}`, {}) : get(item, propertyKey, {});
|
||||
|
||||
const rawSettings = getPropertyFromDraftOrRequest('settings');
|
||||
const settings = { ...DEFAULT_SETTINGS, ...rawSettings };
|
||||
const { encodeUrl, followRedirects, maxRedirects, timeout } = settings;
|
||||
const { encodeUrl } = getPropertyFromDraftOrRequest('settings');
|
||||
|
||||
// Reusable function to update settings
|
||||
const updateSetting = useCallback((settingUpdate) => {
|
||||
const updatedSettings = { ...settings, ...settingUpdate };
|
||||
const onToggleUrlEncoding = useCallback(() => {
|
||||
dispatch(updateItemSettings({
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
settings: updatedSettings
|
||||
settings: { encodeUrl: !encodeUrl }
|
||||
}));
|
||||
}, [dispatch, collection.uid, item.uid, settings]);
|
||||
|
||||
// Setting change handlers
|
||||
const onToggleUrlEncoding = useCallback(() =>
|
||||
updateSetting({ encodeUrl: !encodeUrl }), [encodeUrl, updateSetting]);
|
||||
|
||||
const onToggleFollowRedirects = useCallback(() =>
|
||||
updateSetting({ followRedirects: !followRedirects }), [followRedirects, updateSetting]);
|
||||
|
||||
const onMaxRedirectsChange = useCallback((e) => {
|
||||
const value = e.target.value;
|
||||
// Only allow empty string or digits
|
||||
if (value === '' || /^\d+$/.test(value)) {
|
||||
const numericValue = value === '' ? 0 : parseInt(value, 10);
|
||||
updateSetting({ maxRedirects: numericValue });
|
||||
}
|
||||
}, [updateSetting]);
|
||||
|
||||
const onTimeoutChange = useCallback((e) => {
|
||||
const value = e.target.value;
|
||||
// Only allow empty string or digits
|
||||
if (value === '' || /^\d+$/.test(value)) {
|
||||
const numericValue = value === '' ? 0 : parseInt(value, 10);
|
||||
updateSetting({ timeout: numericValue });
|
||||
}
|
||||
}, [updateSetting]);
|
||||
|
||||
// Check if timeout is inherited
|
||||
const isTimeoutInherited = timeout === 'inherit' || timeout === undefined || timeout === null;
|
||||
|
||||
const handleTimeoutDropdownSelect = useCallback((option) => {
|
||||
if (option === 'inherit') {
|
||||
updateSetting({ timeout: 'inherit' });
|
||||
} else if (option === 'custom') {
|
||||
// Switch to custom value - start with 0
|
||||
updateSetting({ timeout: 0 });
|
||||
}
|
||||
}, [updateSetting]);
|
||||
|
||||
// Keyboard shortcut handlers
|
||||
const onSave = useCallback(() => {
|
||||
dispatch(saveRequest(item.uid, collection.uid));
|
||||
}, [dispatch, item.uid, collection.uid]);
|
||||
|
||||
const onRun = useCallback(() => {
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
}, [dispatch, item, collection.uid]);
|
||||
|
||||
// Keyboard shortcut handler for input fields
|
||||
const handleKeyDown = useCallback((e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||
e.preventDefault();
|
||||
onSave();
|
||||
} else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
onRun();
|
||||
}
|
||||
}, [onSave, onRun]);
|
||||
}, [encodeUrl, dispatch, collection.uid, item.uid]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">Configure request settings for this item.</div>
|
||||
<div className="bruno-form">
|
||||
<div className="mb-6">
|
||||
<h3 className="text-xs font-medium text-gray-900 dark:text-gray-100 flex items-center gap-1 mb-4">
|
||||
<IconTag size={16} />
|
||||
Tags
|
||||
</h3>
|
||||
<div className="w-full h-full flex flex-col gap-10">
|
||||
<div className='flex flex-col gap-2 max-w-[400px]'>
|
||||
<h3 className="text-xs font-medium text-gray-900 dark:text-gray-100 flex items-center gap-1">
|
||||
<IconTag size={16} />
|
||||
Tags
|
||||
</h3>
|
||||
<div label="Tags">
|
||||
<Tags item={item} collection={collection} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<ToggleSelector
|
||||
checked={encodeUrl}
|
||||
onChange={onToggleUrlEncoding}
|
||||
label="URL Encoding"
|
||||
description="Automatically encode query parameters in the URL"
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<ToggleSelector
|
||||
checked={followRedirects}
|
||||
onChange={onToggleFollowRedirects}
|
||||
label="Automatically Follow Redirects"
|
||||
description="Follow HTTP redirects automatically"
|
||||
size="medium"
|
||||
data-testid="follow-redirects-toggle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SettingsInput
|
||||
id="maxRedirects"
|
||||
label="Max Redirects"
|
||||
value={maxRedirects}
|
||||
onChange={onMaxRedirectsChange}
|
||||
description="Set a limit for the number of redirects to follow"
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
|
||||
<InheritableSettingsInput
|
||||
id="timeout"
|
||||
label="Timeout (ms)"
|
||||
value={timeout}
|
||||
description="Set maximum time to wait before aborting the request"
|
||||
onKeyDown={handleKeyDown}
|
||||
isInherited={isTimeoutInherited}
|
||||
onDropdownSelect={handleTimeoutDropdownSelect}
|
||||
onValueChange={(e) => !isTimeoutInherited && onTimeoutChange(e)}
|
||||
onCustomValueReset={() => updateSetting({ timeout: 'inherit' })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<ToggleSelector
|
||||
checked={encodeUrl}
|
||||
onChange={onToggleUrlEncoding}
|
||||
label="URL Encoding"
|
||||
description="Automatically encode query parameters in the URL"
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
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;
|
||||
@@ -1,9 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.inherit-mode-text {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,85 +0,0 @@
|
||||
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 AUTH_MODES = [
|
||||
{
|
||||
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 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 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">
|
||||
{AUTH_MODES.map((authMode) => (
|
||||
<div
|
||||
key={authMode.mode}
|
||||
className="dropdown-item"
|
||||
onClick={() => onClickHandler(authMode.mode)}
|
||||
>
|
||||
{authMode.name}
|
||||
</div>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WSAuthMode;
|
||||
@@ -1,129 +0,0 @@
|
||||
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 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';
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
<div>
|
||||
OAuth 2 not <strong>yet</strong> supported by WebSockets. Using no auth instead.
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
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 WebSockets. 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;
|
||||
@@ -1,120 +0,0 @@
|
||||
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';
|
||||
import WSSettingsPane from '../WSSettingsPane/index';
|
||||
|
||||
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 Headers" />;
|
||||
}
|
||||
case 'settings': {
|
||||
return <WSSettingsPane item={item} collection={collection} />;
|
||||
}
|
||||
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')}>
|
||||
Headers
|
||||
{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;
|
||||
@@ -1,29 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.single-line-editor-wrapper {
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
|
||||
&.error{
|
||||
border-color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-mod {
|
||||
font-size: 11px !important;
|
||||
width: 150px !important;
|
||||
|
||||
& ul {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
& ul > li {
|
||||
list-style: circle;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,135 +0,0 @@
|
||||
import cn from 'classnames';
|
||||
import InfoTip from 'components/InfoTip/index';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
import { useFormik } from 'formik';
|
||||
import get from 'lodash/get';
|
||||
import { updateItemSettings } from 'providers/ReduxStore/slices/collections';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import * as Yup from 'yup';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
/**
|
||||
* @param {string} propertyKey
|
||||
* @param {{draft?:Record<string,unknown>}} item
|
||||
* @returns
|
||||
*/
|
||||
const getPropertyFromDraftOrRequest = (propertyKey, item) =>
|
||||
item.draft ? get(item, `draft.${propertyKey}`, {}) : get(item, propertyKey, {});
|
||||
|
||||
const ERRORS = {
|
||||
timeout: {
|
||||
invalid: `Timeout needs to be a valid number`
|
||||
},
|
||||
keepAliveInterval: {
|
||||
invalid: `Timeout needs to be a valid number`
|
||||
}
|
||||
};
|
||||
|
||||
const WSSettingsPane = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const requestPreferences = useSelector((state) => state.app.preferences.request);
|
||||
|
||||
const { timeout: _connectionTimeout, keepAliveInterval = 0 } = getPropertyFromDraftOrRequest('settings', item);
|
||||
|
||||
const connectionTimeout = _connectionTimeout ?? requestPreferences.timeout;
|
||||
|
||||
const updateSetting = (key, value) => {
|
||||
dispatch(updateItemSettings({
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
settings: {
|
||||
[key]: value
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const formErrors = {
|
||||
timeout: isNaN(Number(connectionTimeout)) && ERRORS.timeout.invalid,
|
||||
keepAliveInterval: isNaN(Number(keepAliveInterval)) && ERRORS.keepAliveInterval.invalid
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col mt-4 gap-4 w-full">
|
||||
<section className="grid gap-4 items-center grid-cols-2">
|
||||
<div>
|
||||
<label className="font-medium mb-2">Timeout</label>
|
||||
<InfoTip
|
||||
infotipId="setting-connection-timeout"
|
||||
className="tooltip-mod max-w-lg"
|
||||
content={(
|
||||
<div>
|
||||
<p>
|
||||
<span>Timeout in milliseconds</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={cn('single-line-editor-wrapper', {
|
||||
error: formErrors.timeout
|
||||
})}
|
||||
>
|
||||
<ToolHint
|
||||
key="timeout"
|
||||
toolhintId="ws-settings-timeout"
|
||||
place="top"
|
||||
text={formErrors.timeout ? formErrors.timeout : ''}
|
||||
>
|
||||
<SingleLineEditor
|
||||
value={connectionTimeout}
|
||||
theme={storedTheme}
|
||||
onChange={(newValue) => updateSetting('timeout', newValue)}
|
||||
collection={collection}
|
||||
/>
|
||||
</ToolHint>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="font-medium mb-2">Keep Alive Interval</label>
|
||||
<InfoTip
|
||||
infotipId="setting-keep-alive"
|
||||
className="tooltip-mod max-w-lg"
|
||||
content={(
|
||||
<div>
|
||||
<p>
|
||||
<span>
|
||||
Keep the websocket alive by sending ping requests to the server at every interval (in millseconds)
|
||||
</span>
|
||||
</p>
|
||||
<p className="mt-2">0 (zero) = off</p>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={cn('single-line-editor-wrapper', {
|
||||
error: formErrors.keepAliveInterval
|
||||
})}
|
||||
>
|
||||
<ToolHint
|
||||
key="timeout"
|
||||
toolhintId="ws-settings-keepAliveInterval"
|
||||
place="top"
|
||||
text={formErrors.keepAliveInterval ? formErrors.keepAliveInterval : ''}
|
||||
>
|
||||
<SingleLineEditor
|
||||
value={keepAliveInterval}
|
||||
theme={storedTheme}
|
||||
onChange={(newValue) => updateSetting('keepAliveInterval', newValue)}
|
||||
collection={collection}
|
||||
/>
|
||||
</ToolHint>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WSSettingsPane;
|
||||
@@ -1,30 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
|
||||
.body-mode-selector {
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
padding-left: 1.5rem !important;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.selected-body-mode {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,58 +0,0 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { humanizeRequestBodyMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RAW_MODES = [
|
||||
{
|
||||
label: 'JSON',
|
||||
key: 'json'
|
||||
},
|
||||
{
|
||||
label: 'XML',
|
||||
key: 'xml'
|
||||
},
|
||||
{
|
||||
label: 'TEXT',
|
||||
key: 'text'
|
||||
}
|
||||
];
|
||||
|
||||
const WSRequestBodyMode = ({ mode, onModeChange }) => {
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none selected-body-mode">
|
||||
{humanizeRequestBodyMode(mode)}
|
||||
{' '}
|
||||
<IconCaretDown className="caret ml-2" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer body-mode-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div className="label-item font-medium">Raw</div>
|
||||
{RAW_MODES.map((d) => (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
key={d.key}
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange(d.key);
|
||||
}}
|
||||
>
|
||||
{d.label}
|
||||
</div>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default WSRequestBodyMode;
|
||||
@@ -1,203 +0,0 @@
|
||||
import { IconChevronDown, IconChevronUp, IconTrash, IconWand } from '@tabler/icons';
|
||||
import CodeEditor from 'components/CodeEditor/index';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
import { get } from 'lodash';
|
||||
import invert from 'lodash/invert';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import React, { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { autoDetectLang } from 'utils/codemirror/lang-detect';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import { prettifyJSON } from 'utils/common/index';
|
||||
import xmlFormat from 'xml-formatter';
|
||||
import WSRequestBodyMode from '../BodyMode/index';
|
||||
|
||||
export const TYPE_BY_DECODER = {
|
||||
base64: 'binary',
|
||||
json: 'json',
|
||||
xml: 'xml'
|
||||
};
|
||||
|
||||
export const DECODER_BY_TYPE = invert(TYPE_BY_DECODER);
|
||||
|
||||
export const SingleWSMessage = ({
|
||||
message,
|
||||
item,
|
||||
collection,
|
||||
index,
|
||||
methodType,
|
||||
isCollapsed,
|
||||
onToggleCollapse,
|
||||
handleRun,
|
||||
canClientSendMultipleMessages
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
|
||||
const { name, content, type } = message;
|
||||
const [messageFormat, setMessageFormat] = useState(autoDetectLang(content));
|
||||
|
||||
const onUpdateMessageType = (type) => {
|
||||
setMessageFormat(type);
|
||||
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
|
||||
currentMessages[index] = {
|
||||
...currentMessages[index],
|
||||
type: DECODER_BY_TYPE[type]
|
||||
};
|
||||
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
};
|
||||
|
||||
const onEdit = (value) => {
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
|
||||
currentMessages[index] = {
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
type: DECODER_BY_TYPE[messageFormat],
|
||||
content: value
|
||||
};
|
||||
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const onDeleteMessage = () => {
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
|
||||
currentMessages.splice(index, 1);
|
||||
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
};
|
||||
|
||||
const getContainerHeight
|
||||
= canClientSendMultipleMessages && body.ws.length > 1 ? `${isCollapsed ? '' : 'h-80'}` : 'h-full';
|
||||
|
||||
let codeType = messageFormat;
|
||||
if (TYPE_BY_DECODER[type]) {
|
||||
codeType = TYPE_BY_DECODER[type];
|
||||
}
|
||||
|
||||
const codemirrorMode = {
|
||||
text: 'application/text',
|
||||
xml: 'application/xml',
|
||||
json: 'application/ld+json'
|
||||
};
|
||||
|
||||
const onPrettify = () => {
|
||||
if (codeType === 'json') {
|
||||
try {
|
||||
const prettyBodyJson = prettifyJSON(content);
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
currentMessages[index] = {
|
||||
...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.'));
|
||||
}
|
||||
}
|
||||
|
||||
if (codeType === 'xml') {
|
||||
try {
|
||||
const prettyBodyXML = xmlFormat(content, { collapseContent: true });
|
||||
|
||||
const currentMessages = [...(body.ws || [])];
|
||||
currentMessages[index] = {
|
||||
...currentMessages[index],
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
content: prettyBodyXML
|
||||
};
|
||||
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
} catch (e) {
|
||||
toastError(new Error('Unable to prettify. Invalid XML format.'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<WSRequestBodyMode mode={messageFormat} onModeChange={onUpdateMessageType} />
|
||||
<ToolHint text="Prettify" 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>
|
||||
|
||||
{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={codemirrorMode[codeType] ?? 'text/plain'}
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -1,116 +0,0 @@
|
||||
import { get } from 'lodash';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
|
||||
import { IconPlus } from '@tabler/icons';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { SingleWSMessage } from './SingleWSMessage/index';
|
||||
|
||||
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="ws-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;
|
||||
@@ -1,103 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
height: 2.3rem;
|
||||
position: relative;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.method-ws {
|
||||
color: ${(props) => props.theme.request.ws};
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -1,170 +0,0 @@
|
||||
import { IconArrowRight, IconDeviceFloppy, IconPlugConnected, IconPlugConnectedX } from '@tabler/icons';
|
||||
import { IconWebSocket } from 'components/Icons/Grpc';
|
||||
import classnames from 'classnames';
|
||||
import SingleLineEditor from 'components/SingleLineEditor/index';
|
||||
import { requestUrlChanged } from 'providers/ReduxStore/slices/collections';
|
||||
import { wsConnectOnly, 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, isWsConnectionActive } from 'utils/network/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const WsQueryUrl = ({ item, collection, handleRun }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { theme, displayedTheme } = useTheme();
|
||||
const [isConnectionActive, setIsConnectionActive] = useState(false);
|
||||
// TODO: reaper, better state for connecting
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const url = getPropertyFromDraftOrRequest(item, 'request.url');
|
||||
const response = item.draft ? get(item, 'draft.response', {}) : get(item, 'response', {});
|
||||
const saveShortcut = isMacOS() ? '⌘S' : 'Ctrl+S';
|
||||
|
||||
const showConnectingPulse = isConnecting && response.status !== 'CLOSED';
|
||||
|
||||
// Check connection status
|
||||
useEffect(() => {
|
||||
const checkConnectionStatus = async () => {
|
||||
try {
|
||||
const result = await isWsConnectionActive(item.uid);
|
||||
const active = Boolean(result.isActive);
|
||||
setIsConnectionActive(active);
|
||||
setIsConnecting(false);
|
||||
} catch (error) {
|
||||
setIsConnectionActive(false);
|
||||
setIsConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkConnectionStatus();
|
||||
const interval = setInterval(checkConnectionStatus, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, [item.uid]);
|
||||
|
||||
const onUrlChange = (value) => {
|
||||
closeWsConnection(item.uid);
|
||||
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);
|
||||
setIsConnecting(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) => {
|
||||
setIsConnecting(true);
|
||||
dispatch(wsConnectOnly(item, collection.uid));
|
||||
};
|
||||
|
||||
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 pr-2 h-full relative">
|
||||
<div className="flex items-center justify-center w-16">
|
||||
<span className="text-xs font-bold method-ws">WS</span>
|
||||
</div>
|
||||
<SingleLineEditor
|
||||
value={url}
|
||||
onSave={(finalValue) => onSave(finalValue)}
|
||||
onChange={onUrlChange}
|
||||
placeholder="ws://localhost:8080 or wss://example.com"
|
||||
className="w-full"
|
||||
theme={displayedTheme}
|
||||
onRun={handleRun}
|
||||
/>
|
||||
<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.colors.text.danger}
|
||||
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={
|
||||
classnames('cursor-pointer', {
|
||||
'animate-pulse': showConnectingPulse
|
||||
})
|
||||
}
|
||||
color={theme.colors.text.green}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
/>
|
||||
<span className="infotip-text text-xs">Connect</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div data-testid="run-button" 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,9 +29,6 @@ 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;
|
||||
@@ -121,7 +118,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;
|
||||
@@ -188,7 +185,6 @@ 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} />;
|
||||
@@ -233,7 +229,6 @@ 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) {
|
||||
@@ -246,11 +241,6 @@ 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
|
||||
@@ -258,21 +248,14 @@ 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} />
|
||||
}
|
||||
{isGrpcRequest ? (
|
||||
<GrpcQueryUrl 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 className="request-pane">
|
||||
@@ -282,7 +265,6 @@ const RequestTabPanel = () => {
|
||||
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`
|
||||
}}
|
||||
@@ -304,10 +286,6 @@ const RequestTabPanel = () => {
|
||||
{isGrpcRequest ? (
|
||||
<GrpcRequestPane item={item} collection={collection} handleRun={handleRun} />
|
||||
) : null}
|
||||
|
||||
{isWsRequest ? (
|
||||
<WSRequestPane item={item} collection={collection} handleRun={handleRun} />
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -320,12 +298,7 @@ const RequestTabPanel = () => {
|
||||
<GrpcResponsePane
|
||||
item={item}
|
||||
collection={collection}
|
||||
response={item.response}
|
||||
/>
|
||||
) : item.type === 'ws-request' ? (
|
||||
<WSResponsePane
|
||||
item={item}
|
||||
collection={collection}
|
||||
|
||||
response={item.response}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
|
||||
const DraggableTab = ({ id, onMoveTab, index, children, className, onClick }) => {
|
||||
const ref = React.useRef(null);
|
||||
|
||||
const [{ handlerId, isOver }, drop] = useDrop({
|
||||
accept: 'tab',
|
||||
hover(item, monitor) {
|
||||
onMoveTab(item.id, id);
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
handlerId: monitor.getHandlerId(),
|
||||
isOver: monitor.isOver()
|
||||
})
|
||||
});
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: 'tab',
|
||||
item: () => {
|
||||
return { id, index };
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging()
|
||||
}),
|
||||
options: {
|
||||
dropEffect: 'move'
|
||||
}
|
||||
});
|
||||
|
||||
drag(drop(ref));
|
||||
|
||||
return (
|
||||
<li
|
||||
className={className}
|
||||
ref={ref}
|
||||
role="tab"
|
||||
style={{ opacity: isDragging || isOver ? 0 : 1 }}
|
||||
onClick={onClick}
|
||||
data-handler-id={handlerId}
|
||||
>
|
||||
{children}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default DraggableTab;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState, useRef, Fragment } from 'react';
|
||||
import React, { useState, useRef, Fragment } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -18,7 +18,6 @@ import NewRequest from 'components/Sidebar/NewRequest/index';
|
||||
import CloseTabIcon from './CloseTabIcon';
|
||||
import DraftTabIcon from './DraftTabIcon';
|
||||
import { flattenItems } from 'utils/collections/index';
|
||||
import { closeWsConnection } from 'utils/network/index';
|
||||
|
||||
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -67,13 +66,10 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
};
|
||||
|
||||
const getMethodColor = (method = '') => {
|
||||
const colorMap = {
|
||||
...theme.request.methods,
|
||||
...theme.request
|
||||
};
|
||||
return colorMap[method.toLocaleLowerCase()];
|
||||
return theme.request.methods[method.toLocaleLowerCase()];
|
||||
};
|
||||
|
||||
|
||||
const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
|
||||
if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
|
||||
return (
|
||||
@@ -94,21 +90,6 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
|
||||
const item = findItemInCollection(collection, tab.uid);
|
||||
|
||||
const getMethodText = useCallback((item) => {
|
||||
if (!item) return;
|
||||
|
||||
switch (item.type) {
|
||||
case 'grpc-request':
|
||||
return 'gRPC';
|
||||
case 'ws-request':
|
||||
return 'WS';
|
||||
case 'graphql-request':
|
||||
return 'GQL';
|
||||
default:
|
||||
return item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
|
||||
}
|
||||
}, [item]);
|
||||
|
||||
if (!item) {
|
||||
return (
|
||||
<StyledWrapper
|
||||
@@ -127,8 +108,8 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
);
|
||||
}
|
||||
|
||||
const isWS = item.type === 'ws-request';
|
||||
const method = getMethodText(item);
|
||||
const isGrpc = item.type === 'grpc-request';
|
||||
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
@@ -137,7 +118,6 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
item={item}
|
||||
onCancel={() => setShowConfirmClose(false)}
|
||||
onCloseWithoutSave={() => {
|
||||
isWS && closeWsConnection(item.uid);
|
||||
dispatch(
|
||||
deleteRequestDraft({
|
||||
itemUid: item.uid,
|
||||
@@ -181,8 +161,8 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="tab-method uppercase" style={{ color: getMethodColor(method), fontSize: 12 }}>
|
||||
{method}
|
||||
<span className="tab-method uppercase" style={{ color: isGrpc ? theme.request.grpc : getMethodColor(method), fontSize: 12 }}>
|
||||
{isGrpc ? 'gRPC' : method}
|
||||
</span>
|
||||
<span className="ml-1 tab-name" title={item.name}>
|
||||
{item.name}
|
||||
@@ -200,10 +180,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
<div
|
||||
className="flex px-2 close-icon-container"
|
||||
onClick={(e) => {
|
||||
if (!item.draft) {
|
||||
isWS && closeWsConnection(item.uid);
|
||||
return handleCloseClick(e);
|
||||
};
|
||||
if (!item.draft) return handleCloseClick(e);
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -251,28 +228,6 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
|
||||
function handleRevertChanges(event) {
|
||||
event.stopPropagation();
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
if (!currentTabUid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const item = findItemInCollection(collection, currentTabUid);
|
||||
if (item.draft) {
|
||||
dispatch(
|
||||
deleteRequestDraft({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
function handleCloseOtherTabs(event) {
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
@@ -340,13 +295,6 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
|
||||
>
|
||||
Clone Request
|
||||
</button>
|
||||
<button
|
||||
className="dropdown-item w-full"
|
||||
onClick={handleRevertChanges}
|
||||
disabled={!currentTabItem?.draft}
|
||||
>
|
||||
Revert Changes
|
||||
</button>
|
||||
<button className="dropdown-item w-full" onClick={(e) => handleCloseTab(e, currentTabUid)}>
|
||||
Close
|
||||
</button>
|
||||
|
||||
@@ -4,12 +4,11 @@ import filter from 'lodash/filter';
|
||||
import classnames from 'classnames';
|
||||
import { IconChevronRight, IconChevronLeft } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { focusTab, reorderTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import CollectionToolBar from './CollectionToolBar';
|
||||
import RequestTab from './RequestTab';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import DraggableTab from './DraggableTab';
|
||||
|
||||
const RequestTabs = () => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -107,17 +106,10 @@ const RequestTabs = () => {
|
||||
{collectionRequestTabs && collectionRequestTabs.length
|
||||
? collectionRequestTabs.map((tab, index) => {
|
||||
return (
|
||||
<DraggableTab
|
||||
<li
|
||||
key={tab.uid}
|
||||
id={tab.uid}
|
||||
index={index}
|
||||
onMoveTab={(source, target) => {
|
||||
dispatch(reorderTabs({
|
||||
sourceUid: source,
|
||||
targetUid: target
|
||||
}));
|
||||
}}
|
||||
className={getTabClassname(tab, index)}
|
||||
role="tab"
|
||||
onClick={() => handleClick(tab)}
|
||||
>
|
||||
<RequestTab
|
||||
@@ -128,7 +120,7 @@ const RequestTabs = () => {
|
||||
collection={activeCollection}
|
||||
folderUid={tab.folderUid}
|
||||
/>
|
||||
</DraggableTab>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { get } from 'lodash';
|
||||
import find from 'lodash/find';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { updateResponsePaneScrollPosition } from 'providers/ReduxStore/slices/tabs';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { Document, Page } from 'react-pdf';
|
||||
import 'pdfjs-dist/build/pdf.worker';
|
||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||
@@ -76,8 +76,6 @@ const QueryResultPreview = ({
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const onScroll = (event) => {
|
||||
dispatch(
|
||||
updateResponsePaneScrollPosition({
|
||||
@@ -129,7 +127,6 @@ const QueryResultPreview = ({
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
theme={displayedTheme}
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
onScroll={onScroll}
|
||||
value={formattedData}
|
||||
mode={mode}
|
||||
|
||||
@@ -11,7 +11,7 @@ const ResponseSave = ({ item }) => {
|
||||
const saveResponseToFile = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-response-to-file', response, item?.requestSent?.url, item.pathname)
|
||||
.invoke('renderer:save-response-to-file', response, item?.requestSent?.url)
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error(get(err, 'error.message') || 'Something went wrong!');
|
||||
|
||||
@@ -76,39 +76,35 @@ describe('ResponseSize', () => {
|
||||
});
|
||||
|
||||
it('should handle exactly 1024 bytes as size', () => {
|
||||
const size = 1024;
|
||||
renderWithTheme(<ResponseSize size={size} />);
|
||||
renderWithTheme(<ResponseSize size={1024} />);
|
||||
const element = screen.getByText(/1024B/);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.textContent).toMatch(/^1024B$/);
|
||||
expect(element).toHaveAttribute('title', `${size.toLocaleString()}B`);
|
||||
expect(element).toHaveAttribute('title', '1,024B');
|
||||
});
|
||||
|
||||
it('should render kilobytes when size is greater than 1024', () => {
|
||||
const size = 1500;
|
||||
renderWithTheme(<ResponseSize size={size} />);
|
||||
renderWithTheme(<ResponseSize size={1500} />);
|
||||
const element = screen.getByText(/1\.46KB/);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.textContent).toMatch(/^\d+\.\d+KB$/);
|
||||
expect(element).toHaveAttribute('title', `${size.toLocaleString()}B`);
|
||||
expect(element).toHaveAttribute('title', '1,500B');
|
||||
});
|
||||
|
||||
it('should handle large size numbers', () => {
|
||||
const size = 10240;
|
||||
renderWithTheme(<ResponseSize size={size} />);
|
||||
renderWithTheme(<ResponseSize size={10240} />);
|
||||
const element = screen.getByText(/10\.0KB/);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.textContent).toMatch(/^\d+\.\d+KB$/);
|
||||
expect(element).toHaveAttribute('title', `${size.toLocaleString()}B`);
|
||||
expect(element).toHaveAttribute('title', '10,240B');
|
||||
});
|
||||
|
||||
it('should handle decimal size numbers', () => {
|
||||
const size = 1126.5;
|
||||
renderWithTheme(<ResponseSize size={size} />);
|
||||
renderWithTheme(<ResponseSize size={1126.5} />);
|
||||
const element = screen.getByText(/1\.10KB/);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.textContent).toMatch(/^\d+\.\d+KB$/);
|
||||
expect(element).toHaveAttribute('title', `${size.toLocaleString()}B`);
|
||||
expect(element).toHaveAttribute('title', '1,126.5B');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -45,7 +45,7 @@ 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' || item.type === 'ws-request';
|
||||
const isGrpcRequest = item.type === 'grpc-request';
|
||||
|
||||
// Filter timeline entries based on new rules
|
||||
const combinedTimeline = ([...(collection?.timeline || [])]).filter(obj => {
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
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;
|
||||
@@ -1,44 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
overflow-y: auto;
|
||||
|
||||
.ws-message.new {
|
||||
background-color: ${({ theme }) => theme.table.striped};
|
||||
}
|
||||
|
||||
.ws-message:not(:last-child) {
|
||||
border-bottom: 1px solid ${({ theme }) => theme.table.border};
|
||||
}
|
||||
|
||||
.ws-message:not(:last-child).open {
|
||||
border-bottom-width: 0px;
|
||||
}
|
||||
|
||||
.ws-incoming {
|
||||
background: ${(props) => props.theme.bg};
|
||||
border-color: ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
.ws-outgoing {
|
||||
background: ${(props) => props.theme.bg};
|
||||
border-color: ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.CodeMirror-foldgutter, .CodeMirror-linenumbers, .CodeMirror-lint-markers {
|
||||
background: ${({ theme }) => theme.bg};
|
||||
}
|
||||
|
||||
div[role='tablist'] {
|
||||
.active {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,204 +0,0 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconExclamationCircle, IconChevronRight, IconInfoCircle, IconChevronDown, IconArrowUpRight, IconArrowDownLeft } from '@tabler/icons';
|
||||
import CodeEditor from 'components/CodeEditor/index';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import _ from 'lodash';
|
||||
import { useRef } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const getContentMeta = (content) => {
|
||||
if (typeof content === 'object') {
|
||||
return {
|
||||
isJSON: true,
|
||||
content: JSON.stringify(content, null, 0)
|
||||
};
|
||||
}
|
||||
try {
|
||||
return {
|
||||
isJSON: true,
|
||||
content: JSON.stringify(JSON.parse(content), null, 0)
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
isJSON: false,
|
||||
content: content
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const parseContent = (content) => {
|
||||
let contentMeta = getContentMeta(content);
|
||||
return {
|
||||
type: contentMeta.isJSON ? 'application/json' : 'text/plain',
|
||||
content: contentMeta.isJSON ? JSON.stringify(JSON.parse(contentMeta.content), null, 2) : contentMeta.content
|
||||
};
|
||||
};
|
||||
|
||||
const getDataTypeText = (type) => {
|
||||
const textMap = {
|
||||
'text/plain': 'RAW',
|
||||
'application/json': 'JSON'
|
||||
};
|
||||
return textMap[type] ?? 'RAW';
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {"incoming"|"outgoing"|"info"} type
|
||||
*/
|
||||
const TypeIcon = ({ type }) => {
|
||||
const commonProps = {
|
||||
size: 18
|
||||
};
|
||||
return {
|
||||
incoming: <IconArrowDownLeft {...commonProps} />,
|
||||
outgoing: <IconArrowUpRight {...commonProps} />,
|
||||
info: <IconInfoCircle {...commonProps} />,
|
||||
error: <IconExclamationCircle {...commonProps} />
|
||||
}[type];
|
||||
};
|
||||
|
||||
const WSMessageItem = ({ message, inFocus }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [showHex, setShowHex] = useState(false);
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const { displayedTheme } = useTheme();
|
||||
const [isNew, setIsNew] = useState(false);
|
||||
const notified = useRef(false);
|
||||
|
||||
const isIncoming = message.type === 'incoming';
|
||||
const isInfo = message.type === 'info';
|
||||
const isError = message.type === 'error';
|
||||
const isOutgoing = message.type === 'outgoing';
|
||||
let contentHexdump = message.messageHexdump;
|
||||
let parsedContent = parseContent(message.message);
|
||||
const dataType = getDataTypeText(parsedContent.type);
|
||||
|
||||
useEffect(() => {
|
||||
if (notified.current === true) return;
|
||||
const dateDiff = Date.now() - new Date(message.timestamp).getTime();
|
||||
if (dateDiff < 1000 * 10) {
|
||||
setIsNew(true);
|
||||
setTimeout(() => {
|
||||
notified.current = true;
|
||||
setIsNew(false);
|
||||
}, 2500);
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
const canOpenMessage = !isInfo && !isError;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(node) => {
|
||||
if (!node) return;
|
||||
if (inFocus) node.scrollIntoView();
|
||||
}}
|
||||
className={classnames('ws-message flex flex-col p-2', {
|
||||
'ws-incoming': isIncoming,
|
||||
'ws-outgoing': !isIncoming,
|
||||
'open': isOpen,
|
||||
'new': isNew
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classnames('flex items-center justify-between', {
|
||||
'cursor-pointer': !isInfo,
|
||||
'cursor-not-allowed': isInfo
|
||||
})}
|
||||
onClick={(e) => {
|
||||
if (!canOpenMessage) return;
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
>
|
||||
<div className="flex min-w-0 shrink">
|
||||
<span
|
||||
className={classnames('font-semibold flex items-center gap-1',
|
||||
{
|
||||
'text-green-700': isIncoming,
|
||||
'text-yellow-700': isOutgoing,
|
||||
'text-blue-700': isInfo,
|
||||
'text-red-700': isError,
|
||||
'text-red-700': isError
|
||||
})}
|
||||
>
|
||||
<TypeIcon type={message.type} />
|
||||
</span>
|
||||
<span className="ml-3 text-ellipsis max-w-full overflow-hidden text-nowrap">{parsedContent.content}</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 gap-2">
|
||||
{message.timestamp && (
|
||||
<span className="text-xs text-gray-400">{new Date(message.timestamp).toISOString()}</span>
|
||||
)}
|
||||
{canOpenMessage
|
||||
? (
|
||||
<span className="text-gray-600">
|
||||
{isOpen ? (
|
||||
<IconChevronDown size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
) : (
|
||||
<IconChevronRight size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
: <span class="w-4"></span>}
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<>
|
||||
<div className="mt-2 flex justify-end gap-2 text-xs ws-message-toolbar" role="tablist">
|
||||
<div
|
||||
className={classnames('select-none capitalize', {
|
||||
'active': showHex,
|
||||
'cursor-pointer': !showHex
|
||||
})}
|
||||
role="tab"
|
||||
onClick={() => setShowHex(true)}
|
||||
>
|
||||
hexdump
|
||||
</div>
|
||||
<div
|
||||
className={classnames('select-none capitalize', {
|
||||
'active': !showHex,
|
||||
'cursor-pointer': showHex
|
||||
})}
|
||||
role="tab"
|
||||
onClick={() => setShowHex(false)}
|
||||
>
|
||||
{dataType.toLowerCase()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 h-[300px] w-full">
|
||||
<CodeEditor
|
||||
mode={showHex ? 'text/plain' : parsedContent.type}
|
||||
theme={displayedTheme}
|
||||
enableLineWrapping={showHex ? false : true}
|
||||
font={preferences.codeFont || 'default'}
|
||||
value={showHex ? contentHexdump : parsedContent.content}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WSMessagesList = ({ order = -1, messages = [] }) => {
|
||||
if (!messages.length) {
|
||||
return <div className="p-4 text-gray-500">No messages yet.</div>;
|
||||
}
|
||||
const ordered = order === -1 ? messages : messages.slice().reverse();
|
||||
return (
|
||||
<StyledWrapper className="ws-messages-list mt-1 flex flex-col">
|
||||
{ordered.map((msg, idx, src) => {
|
||||
const inFocus = order === -1 ? src.length - 1 === idx : idx === 0;
|
||||
return <WSMessageItem inFocus={inFocus} id={idx} message={msg} />;
|
||||
})}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WSMessagesList;
|
||||
@@ -1,31 +0,0 @@
|
||||
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;
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WSResponseHeaders = ({ response }) => {
|
||||
const formatHeaders = (headers) => {
|
||||
if (!headers) return [];
|
||||
if (Array.isArray(headers)) return headers;
|
||||
return Object.entries(headers).map(([key, value]) => ({ name: key, value }));
|
||||
};
|
||||
|
||||
const headersArray = formatHeaders(response.headers);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="pb-4 w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{headersArray && headersArray.length ? (
|
||||
headersArray.map((header, index) => (
|
||||
<tr key={index}>
|
||||
<td className="key">{header.name}</td>
|
||||
<td className="value">{header.value}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="2" className="text-center py-4 text-gray-500">
|
||||
No headers received
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WSResponseHeaders;
|
||||
@@ -1,8 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
color: ${(props) => props.theme.requestTabPanel.responseStatus};
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconSortDescending2, IconSortAscending2 } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { wsUpdateResponseSortOrder } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const WSResponseSortOrder = ({ collection, item }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const order = item.response?.sortOrder ?? -1;
|
||||
|
||||
const toggleSortOrder = () => {
|
||||
dispatch(wsUpdateResponseSortOrder({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="ml-2 flex items-center">
|
||||
<button onClick={toggleSortOrder} title={order === -1 ? 'Latest Last' : 'Latest First'}>
|
||||
{ order === -1
|
||||
? <IconSortDescending2 size={16} strokeWidth={1.5} />
|
||||
: <IconSortAscending2 size={16} strokeWidth={1.5} />}
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WSResponseSortOrder;
|
||||
@@ -1,22 +0,0 @@
|
||||
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;
|
||||
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import wsStatusCodePhraseMap from './get-ws-status-code-phrase';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WSStatusCode = ({ status, text }) => {
|
||||
const getTabClassname = (status) => {
|
||||
return classnames('ml-2', {
|
||||
// ok if normal connect and normal closure
|
||||
'text-ok': parseInt(status) === 0 || parseInt(status) === 1000,
|
||||
'text-error': parseInt(status) !== 1000 && parseInt(status) !== 0
|
||||
});
|
||||
};
|
||||
|
||||
const statusText = text || wsStatusCodePhraseMap[status];
|
||||
|
||||
return (
|
||||
<StyledWrapper className={getTabClassname(status)}>
|
||||
{Number.isInteger(status) && status != 0 ? <div className="mr-1">{status}</div> : null}
|
||||
{statusText && <div>{statusText}</div>}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WSStatusCode;
|
||||
@@ -1,153 +0,0 @@
|
||||
import React from 'react';
|
||||
import find from 'lodash/find';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import Overlay from '../Overlay';
|
||||
import Placeholder from '../Placeholder';
|
||||
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';
|
||||
import WSResponseSortOrder from './WSResponseSortOrder';
|
||||
import WSResponseHeaders from './WSResponseHeaders';
|
||||
|
||||
const WSResult = ({ response }) => {
|
||||
return <WSMessagesList order={response?.sortOrder} messages={response.responses || []} />;
|
||||
};
|
||||
|
||||
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 <WSResult response={response} />;
|
||||
}
|
||||
case 'headers': {
|
||||
return <WSResponseHeaders response={response} />;
|
||||
}
|
||||
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: 'Headers',
|
||||
count: response.headers ? Object.keys(response.headers).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} />
|
||||
<WSResponseSortOrder 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;
|
||||
@@ -1,47 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const SettingsInput = ({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
className = '',
|
||||
description = '',
|
||||
onKeyDown
|
||||
}) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-medium text-gray-900 dark:text-gray-100" htmlFor={id}>
|
||||
{label}
|
||||
</label>
|
||||
{description && (
|
||||
<p className="text-xs text-gray-700 dark:text-gray-400">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
id={id}
|
||||
type="text"
|
||||
className={`block px-2 py-1 rounded-sm outline-none transition-colors duration-100 w-24 h-8 ${className}`}
|
||||
style={{
|
||||
backgroundColor: theme.modal.input.bg,
|
||||
border: `1px solid ${theme.modal.input.border}`
|
||||
}}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsInput;
|
||||
@@ -14,15 +14,9 @@ const ShareCollection = ({ onClose, collectionUid }) => {
|
||||
const collection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid));
|
||||
const isCollectionLoading = areItemsLoading(collection);
|
||||
|
||||
const hasNonExportableRequestTypes = useMemo(() => {
|
||||
let types = new Set();
|
||||
const hasGrpcRequests = useMemo(() => {
|
||||
const checkItem = (item) => {
|
||||
if (item.type === 'grpc-request') {
|
||||
types.add('gRPC');
|
||||
return true;
|
||||
}
|
||||
if (item.type === 'ws-request') {
|
||||
types.add('WebSocket');
|
||||
return true;
|
||||
}
|
||||
if (item.items) {
|
||||
@@ -30,10 +24,7 @@ const ShareCollection = ({ onClose, collectionUid }) => {
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return {
|
||||
has: collection?.items?.filter(checkItem).length || false,
|
||||
types: [...types]
|
||||
};
|
||||
return collection?.items?.some(checkItem) || false;
|
||||
}, [collection]);
|
||||
|
||||
const handleExportBrunoCollection = () => {
|
||||
@@ -84,15 +75,10 @@ const ShareCollection = ({ onClose, collectionUid }) => {
|
||||
}`}
|
||||
onClick={isCollectionLoading ? undefined : handleExportPostmanCollection}
|
||||
>
|
||||
{hasNonExportableRequestTypes.has && (
|
||||
{hasGrpcRequests && (
|
||||
<div className="px-3 py-2 bg-yellow-50 w-full dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-400 text-xs border-b border-yellow-100 dark:border-yellow-800/20 flex items-center">
|
||||
<IconAlertTriangle size={16} className="mr-2 flex-shrink-0" />
|
||||
<span>
|
||||
Note:
|
||||
{hasNonExportableRequestTypes.types.join(', ')}
|
||||
{' '}
|
||||
requests in this collection will not be exported
|
||||
</span>
|
||||
<span>Note: gRPC requests in this collection will not be exported</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center p-3 w-full">
|
||||
|
||||
@@ -12,15 +12,12 @@ import PathDisplay from 'components/PathDisplay';
|
||||
import { useState } from 'react';
|
||||
import { IconArrowBackUp, IconEdit } from "@tabler/icons";
|
||||
import { findCollectionByUid } from 'utils/collections/index';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const CloneCollection = ({ onClose, collectionUid }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
const [isEditing, toggleEditing] = useState(false);
|
||||
const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid));
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const defaultLocation = get(preferences, 'general.defaultCollectionLocation', '');
|
||||
const { name } = collection;
|
||||
|
||||
const formik = useFormik({
|
||||
@@ -28,7 +25,7 @@ const CloneCollection = ({ onClose, collectionUid }) => {
|
||||
initialValues: {
|
||||
collectionName: `${name} copy`,
|
||||
collectionFolderName: `${sanitizeName(name)} copy`,
|
||||
collectionLocation: defaultLocation
|
||||
collectionLocation: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
collectionName: Yup.string()
|
||||
|
||||
@@ -10,6 +10,7 @@ import { findCollectionByItemUid, getGlobalEnvironmentVariables } from 'utils/co
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { generateSnippet } from '../utils/snippet-generator';
|
||||
|
||||
const CodeView = ({ language, item }) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
@@ -32,12 +33,7 @@ const CodeView = ({ language, item }) => {
|
||||
}, [collectionOriginal, globalEnvironments, activeGlobalEnvironmentUid]);
|
||||
|
||||
const snippet = useMemo(() => {
|
||||
return generateSnippet({
|
||||
language,
|
||||
item,
|
||||
collection,
|
||||
shouldInterpolate: generateCodePrefs.shouldInterpolate
|
||||
});
|
||||
return generateSnippet({ language, item, collection, shouldInterpolate: generateCodePrefs.shouldInterpolate });
|
||||
}, [language, item, collection, generateCodePrefs.shouldInterpolate]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -37,12 +37,6 @@ const Wrapper = styled.div`
|
||||
.method-grpc {
|
||||
color: ${(props) => props.theme.request.grpc};
|
||||
}
|
||||
.method-ws {
|
||||
color: ${(props) => props.theme.request.ws};
|
||||
}
|
||||
.method-graphql {
|
||||
color: ${(props) => props.theme.request.gql};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,52 +1,33 @@
|
||||
import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const getMethodFlags = (item) => ({
|
||||
isGrpc: item.type === 'grpc-request',
|
||||
isWS: item.type === 'ws-request',
|
||||
isGraphQL: item.type === 'graphql-request'
|
||||
});
|
||||
|
||||
const getMethodText = (item, { isGrpc, isWS, isGraphQL }) => {
|
||||
if (isGrpc) return 'grpc';
|
||||
if (isWS) return 'ws';
|
||||
if (isGraphQL) return 'gql';
|
||||
return item.request.method.length > 5
|
||||
? item.request.method.substring(0, 3)
|
||||
: item.request.method;
|
||||
};
|
||||
|
||||
const getClassname = (method = '', { isGrpc, isWS, isGraphQL }) => {
|
||||
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,
|
||||
'method-graphql': isGraphQL
|
||||
});
|
||||
};
|
||||
|
||||
const RequestMethod = ({ item }) => {
|
||||
if (!['http-request', 'graphql-request', 'grpc-request', 'ws-request'].includes(item.type)) {
|
||||
if (!['http-request', 'graphql-request', 'grpc-request'].includes(item.type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const flags = getMethodFlags(item);
|
||||
const methodText = getMethodText(item, flags);
|
||||
const className = getClassname(item.request.method, flags);
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className={className}>
|
||||
<div className={getClassname(item.request.method)}>
|
||||
<span className="uppercase">
|
||||
{methodText}
|
||||
{isGrpc ? 'grpc' : item.request.method.length > 5 ? item.request.method.substring(0, 3) : item.request.method}
|
||||
</span>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -6,10 +6,9 @@ import filter from 'lodash/filter';
|
||||
import { useDrop, useDrag } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { toggleCollection, collapseFullCollection } from 'providers/ReduxStore/slices/collections';
|
||||
import { toggleCollection } from 'providers/ReduxStore/slices/collections';
|
||||
import { mountCollection, moveCollectionAndPersist, handleCollectionItemDrop } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
@@ -93,7 +92,6 @@ const Collection = ({ collection, searchText }) => {
|
||||
}
|
||||
|
||||
if(!isChevronClick) {
|
||||
dispatch(hideHomePage()); // @TODO Playwright tests are often stuck on home page, rather than collection settings tab. Revisit for a proper fix.
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: collection.uid,
|
||||
@@ -132,10 +130,6 @@ const Collection = ({ collection, searchText }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCollapseFullCollection = () => {
|
||||
dispatch(collapseFullCollection({ collectionUid: collection.uid }));
|
||||
};
|
||||
|
||||
const viewCollectionSettings = () => {
|
||||
dispatch(
|
||||
addTab({
|
||||
@@ -215,7 +209,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
const folderItems = sortByNameThenSequence(filter(collection.items, (i) => isItemAFolder(i)));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col" id={`collection-${collection.name.replace(/\s+/g, '-').toLowerCase()}`}>
|
||||
<StyledWrapper className="flex flex-col">
|
||||
{showNewRequestModal && <NewRequest collectionUid={collection.uid} onClose={() => setShowNewRequestModal(false)} />}
|
||||
{showNewFolderModal && <NewFolder collectionUid={collection.uid} onClose={() => setShowNewFolderModal(false)} />}
|
||||
{showRenameCollectionModal && (
|
||||
@@ -256,7 +250,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
</div>
|
||||
{isLoading ? <IconLoader2 className="animate-spin mx-1" size={18} strokeWidth={1.5} /> : null}
|
||||
</div>
|
||||
<div className="collection-actions" data-testid="collection-actions">
|
||||
<div className="collection-actions">
|
||||
<Dropdown onCreate={onMenuDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
@@ -278,7 +272,6 @@ const Collection = ({ collection, searchText }) => {
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
data-testid="clone-collection"
|
||||
onClick={(_e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowCloneCollectionModalOpen(true);
|
||||
@@ -315,15 +308,6 @@ const Collection = ({ collection, searchText }) => {
|
||||
>
|
||||
Share
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(_e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
handleCollapseFullCollection();
|
||||
}}
|
||||
>
|
||||
Collapse
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(_e) => {
|
||||
|
||||
@@ -19,10 +19,7 @@ const CreateOrOpenCollection = () => {
|
||||
|
||||
const handleOpenCollection = () => {
|
||||
dispatch(openCollection()).catch(
|
||||
(err) => {
|
||||
console.log(err);
|
||||
toast.error('An error occurred while opening the collection');
|
||||
}
|
||||
(err) => console.log(err) && toast.error('An error occurred while opening the collection')
|
||||
);
|
||||
};
|
||||
const CreateLink = () => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -14,21 +14,18 @@ import Help from 'components/Help';
|
||||
import { multiLineMsg } from "utils/common";
|
||||
import { formatIpcError } from "utils/common/error";
|
||||
import { toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const CreateCollection = ({ onClose }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
const [isEditing, toggleEditing] = useState(false);
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const defaultLocation = get(preferences, 'general.defaultCollectionLocation', '');
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
collectionName: '',
|
||||
collectionFolderName: '',
|
||||
collectionLocation: defaultLocation
|
||||
collectionLocation: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
collectionName: Yup.string()
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { IconLoader2 } from '@tabler/icons';
|
||||
|
||||
// Messages to cycle through while loading
|
||||
const loadingMessages = [
|
||||
'Processing collection...',
|
||||
'Analyzing requests...',
|
||||
'Translating scripts...',
|
||||
'Preparing collection...',
|
||||
'Almost done...'
|
||||
];
|
||||
|
||||
const FullscreenLoader = ({ isLoading }) => {
|
||||
const [loadingMessage, setLoadingMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) return;
|
||||
|
||||
let messageIndex = 0;
|
||||
const interval = setInterval(() => {
|
||||
messageIndex = (messageIndex + 1) % loadingMessages.length;
|
||||
setLoadingMessage(loadingMessages[messageIndex]);
|
||||
}, 2000);
|
||||
|
||||
setLoadingMessage(loadingMessages[0]);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isLoading]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm transition-all duration-300">
|
||||
<div className="flex flex-col items-center p-8 rounded-lg bg-white dark:bg-zinc-800 shadow-lg max-w-md text-center">
|
||||
<IconLoader2 className="animate-spin h-12 w-12 mb-4" strokeWidth={1.5} />
|
||||
<h3 className="text-lg font-medium text-zinc-900 dark:text-zinc-50 mb-2">{loadingMessage}</h3>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
This may take a moment depending on the collection size
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FullscreenLoader;
|
||||
@@ -1,14 +1,12 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { IconFileImport } from '@tabler/icons';
|
||||
import { IconLoader2, IconFileImport } from '@tabler/icons';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { postmanToBruno, isPostmanCollection } from 'utils/importers/postman-collection';
|
||||
import { convertInsomniaToBruno, isInsomniaCollection } from 'utils/importers/insomnia-collection';
|
||||
import { isOpenApiSpec, convertOpenapiToBruno } from 'utils/importers/openapi-collection';
|
||||
import { convertOpenapiToBruno, isOpenApiSpec } from 'utils/importers/openapi-collection';
|
||||
import { processBrunoCollection } from 'utils/importers/bruno-collection';
|
||||
import ImportSettings from 'components/Sidebar/ImportSettings';
|
||||
import FullscreenLoader from './FullscreenLoader/index';
|
||||
|
||||
const convertFileToObject = async (file) => {
|
||||
const text = await file.text();
|
||||
@@ -28,22 +26,60 @@ const convertFileToObject = async (file) => {
|
||||
}
|
||||
};
|
||||
|
||||
const FullscreenLoader = ({ isLoading }) => {
|
||||
const [loadingMessage, setLoadingMessage] = useState('');
|
||||
|
||||
// Messages to cycle through while loading
|
||||
const loadingMessages = [
|
||||
'Processing collection...',
|
||||
'Analyzing requests...',
|
||||
'Translating scripts...',
|
||||
'Preparing collection...',
|
||||
'Almost done...'
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) return;
|
||||
|
||||
let messageIndex = 0;
|
||||
const interval = setInterval(() => {
|
||||
messageIndex = (messageIndex + 1) % loadingMessages.length;
|
||||
setLoadingMessage(loadingMessages[messageIndex]);
|
||||
}, 2000);
|
||||
|
||||
setLoadingMessage(loadingMessages[0]);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isLoading]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm transition-all duration-300">
|
||||
<div className="flex flex-col items-center p-8 rounded-lg bg-white dark:bg-zinc-800 shadow-lg max-w-md text-center">
|
||||
<IconLoader2 className="animate-spin h-12 w-12 mb-4" strokeWidth={1.5} />
|
||||
<h3 className="text-lg font-medium text-zinc-900 dark:text-zinc-50 mb-2">
|
||||
{loadingMessage}
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
This may take a moment depending on the collection size
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const [showImportSettings, setShowImportSettings] = useState(false);
|
||||
const [openApiData, setOpenApiData] = useState(null);
|
||||
const [groupingType, setGroupingType] = useState('tags');
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const handleDrag = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
|
||||
|
||||
if (e.type === 'dragenter' || e.type === 'dragover') {
|
||||
setDragActive(true);
|
||||
} else if (e.type === 'dragleave') {
|
||||
@@ -51,43 +87,30 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportSettings = () => {
|
||||
try {
|
||||
const collection = convertOpenapiToBruno(openApiData, { groupBy: groupingType });
|
||||
handleSubmit({ collection });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toastError(err, 'Failed to process OpenAPI specification');
|
||||
}
|
||||
};
|
||||
|
||||
const processFile = async (file) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await convertFileToObject(file);
|
||||
|
||||
|
||||
if (!data) {
|
||||
throw new Error('Failed to parse file content');
|
||||
}
|
||||
|
||||
// Check if it's an OpenAPI spec and show settings
|
||||
if (isOpenApiSpec(data)) {
|
||||
setOpenApiData(data);
|
||||
setIsLoading(false);
|
||||
setShowImportSettings(true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let collection;
|
||||
|
||||
|
||||
if (isPostmanCollection(data)) {
|
||||
collection = await postmanToBruno(data);
|
||||
} else if (isInsomniaCollection(data)) {
|
||||
}
|
||||
else if (isInsomniaCollection(data)) {
|
||||
collection = convertInsomniaToBruno(data);
|
||||
} else {
|
||||
}
|
||||
else if (isOpenApiSpec(data)) {
|
||||
collection = convertOpenapiToBruno(data);
|
||||
}
|
||||
else {
|
||||
collection = await processBrunoCollection(data);
|
||||
}
|
||||
|
||||
|
||||
handleSubmit({ collection });
|
||||
} catch (err) {
|
||||
toastError(err, 'Import collection failed');
|
||||
@@ -100,7 +123,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragActive(false);
|
||||
|
||||
|
||||
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||
await processFile(e.dataTransfer.files[0]);
|
||||
}
|
||||
@@ -120,23 +143,19 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
return <FullscreenLoader isLoading={isLoading} />;
|
||||
}
|
||||
|
||||
const acceptedFileTypes = ['.json', '.yaml', '.yml', 'application/json', 'application/yaml', 'application/x-yaml'];
|
||||
|
||||
if (showImportSettings) {
|
||||
return (
|
||||
<ImportSettings
|
||||
groupingType={groupingType}
|
||||
setGroupingType={setGroupingType}
|
||||
onClose={onClose}
|
||||
onConfirm={handleImportSettings}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const acceptedFileTypes = [
|
||||
'.json',
|
||||
'.yaml',
|
||||
'.yml',
|
||||
'application/json',
|
||||
'application/yaml',
|
||||
'application/x-yaml'
|
||||
]
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleCancel={onClose} dataTestId="import-collection-modal">
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-4">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Import from file</h3>
|
||||
<div
|
||||
onDragEnter={handleDrag}
|
||||
@@ -145,13 +164,16 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
onDrop={handleDrop}
|
||||
className={`
|
||||
border-2 border-dashed rounded-lg p-6 transition-colors duration-200
|
||||
${dragActive ? 'border-blue-500 bg-blue-50 dark:border-blue-400 dark:bg-blue-900/20' : 'border-gray-200 dark:border-gray-700'}
|
||||
${dragActive
|
||||
? 'border-blue-500 bg-blue-50 dark:border-blue-400 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<IconFileImport
|
||||
size={28}
|
||||
className="text-gray-400 dark:text-gray-500 mb-3"
|
||||
<IconFileImport
|
||||
size={28}
|
||||
className="text-gray-400 dark:text-gray-500 mb-3"
|
||||
/>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.current-group {
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem;
|
||||
cursor: pointer;
|
||||
border: 1px solid ${(props) => props.theme.sidebar.badge.border || 'transparent'};
|
||||
}
|
||||
|
||||
.current-group:hover {
|
||||
background-color: ${(props) => props.theme.sidebar.badge.hoverBg || props.theme.sidebar.badge.bg};
|
||||
}
|
||||
|
||||
/* Fix dropdown positioning */
|
||||
[data-tippy-root] {
|
||||
left: 0 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,84 +0,0 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import Modal from 'components/Modal';
|
||||
import Portal from 'components/Portal';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const groupingOptions = [
|
||||
{ value: 'tags', label: 'Tags', description: 'Group requests by OpenAPI tags', testId: 'grouping-option-tags' },
|
||||
{ value: 'path', label: 'Paths', description: 'Group requests by URL path structure', testId: 'grouping-option-path' }
|
||||
];
|
||||
|
||||
const ImportSettings = ({
|
||||
groupingType,
|
||||
setGroupingType,
|
||||
onClose,
|
||||
onConfirm
|
||||
}) => {
|
||||
const dropdownTippyRef = useRef();
|
||||
|
||||
const onDropdownCreate = (ref) => {
|
||||
dropdownTippyRef.current = ref;
|
||||
};
|
||||
|
||||
const GroupingDropdownIcon = forwardRef((props, ref) => {
|
||||
const selectedOption = groupingOptions.find((option) => option.value === groupingType);
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="flex items-center justify-between w-full current-group"
|
||||
data-testid="grouping-dropdown"
|
||||
>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">{selectedOption.label}</div>
|
||||
</div>
|
||||
<IconCaretDown size={16} className="text-gray-400 ml-[0.25rem]" fill="currentColor" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="sm"
|
||||
title="OpenAPI Import Settings"
|
||||
handleCancel={onClose}
|
||||
handleConfirm={onConfirm}
|
||||
confirmText="Import"
|
||||
dataTestId="import-settings-modal"
|
||||
>
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Folder arrangement</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
Select whether to create folders according to the spec's paths or tags.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<GroupingDropdownIcon />} placement="bottom-start">
|
||||
{groupingOptions.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className="dropdown-item"
|
||||
data-testid={option.testId}
|
||||
onClick={() => {
|
||||
dropdownTippyRef?.current?.hide();
|
||||
setGroupingType(option.value);
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportSettings;
|
||||
@@ -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, newWsRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { newHttpRequest, newGrpcRequest } 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';
|
||||
@@ -21,12 +21,14 @@ import Help from 'components/Help';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import SingleLineEditor from 'components/SingleLineEditor/index';
|
||||
import { useTheme } from 'styled-components';
|
||||
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
|
||||
|
||||
const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
|
||||
const storedTheme = useTheme();
|
||||
const isGrpcEnabled = useBetaFeature(BETA_FEATURES.GRPC);
|
||||
|
||||
const collection = useSelector((state) => state.collections.collections?.find((c) => c.uid === collectionUid));
|
||||
const {
|
||||
@@ -90,13 +92,13 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
|
||||
}
|
||||
|
||||
if (collectionPresets.requestType === 'grpc') {
|
||||
// If gRPC is disabled in beta features, fall back to http-request
|
||||
if (!isGrpcEnabled) {
|
||||
return 'http-request';
|
||||
}
|
||||
return 'grpc-request';
|
||||
}
|
||||
|
||||
if (collectionPresets.requestType === 'ws') {
|
||||
return 'ws-request';
|
||||
}
|
||||
|
||||
return 'http-request';
|
||||
};
|
||||
|
||||
@@ -144,7 +146,6 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
const isGrpcRequest = values.requestType === 'grpc-request';
|
||||
const isWsRequest = values.requestType === 'ws-request';
|
||||
|
||||
if (isGrpcRequest) {
|
||||
dispatch(
|
||||
@@ -164,21 +165,6 @@ 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(
|
||||
@@ -323,81 +309,69 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
|
||||
Type
|
||||
</label>
|
||||
|
||||
<div className="mt-2 grid grid-cols-3 gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<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
|
||||
</label>
|
||||
</div>
|
||||
<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
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<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-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
|
||||
{isGrpcEnabled && (
|
||||
<>
|
||||
<input
|
||||
type="radio"
|
||||
id="grpc-request"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
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
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<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
|
||||
</label>
|
||||
</div>
|
||||
</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'}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<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>
|
||||
<label htmlFor="from-curl" className="ml-1 cursor-pointer select-none">
|
||||
From cURL
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
@@ -488,7 +462,7 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
|
||||
URL
|
||||
</label>
|
||||
<div className="flex items-center mt-2 ">
|
||||
{!['grpc-request', 'ws-request'].includes(formik.values.requestType) ? (
|
||||
{formik.values.requestType !== 'grpc-request' ? (
|
||||
<div className="flex items-center h-full method-selector-container w-1/5">
|
||||
<HttpMethodSelector
|
||||
method={formik.values.requestMethod}
|
||||
|
||||
@@ -55,10 +55,7 @@ const TitleBar = () => {
|
||||
|
||||
const handleOpenCollection = () => {
|
||||
dispatch(openCollection()).catch(
|
||||
(err) => {
|
||||
console.log(err);
|
||||
toast.error('An error occurred while opening the collection');
|
||||
}
|
||||
(err) => console.log(err) && toast.error('An error occurred while opening the collection')
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user