mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 01:18:32 +00:00
Compare commits
246 Commits
feature/pl
...
v2.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
364fb45e97 | ||
|
|
5c9981aca2 | ||
|
|
fc697bf81b | ||
|
|
9bc07afc77 | ||
|
|
e4ae857df3 | ||
|
|
3d26833b8a | ||
|
|
1089a52171 | ||
|
|
9dde2df475 | ||
|
|
1cc94e8ffe | ||
|
|
223f79a3e2 | ||
|
|
5dc6f6757d | ||
|
|
e20fe790a6 | ||
|
|
a006fe8230 | ||
|
|
577d54b432 | ||
|
|
afaebf6b3d | ||
|
|
6e89001825 | ||
|
|
cb611c6510 | ||
|
|
e7dd78ea53 | ||
|
|
9ad0f2d169 | ||
|
|
bf19645282 | ||
|
|
bb01199877 | ||
|
|
5627c5624f | ||
|
|
8e23a7054f | ||
|
|
d820069371 | ||
|
|
6f9daadcfb | ||
|
|
2de9b87c6f | ||
|
|
8d5d952026 | ||
|
|
178773d63a | ||
|
|
7994946c85 | ||
|
|
b020255269 | ||
|
|
afb2d3dffd | ||
|
|
73b0f0919d | ||
|
|
8975b9eef6 | ||
|
|
865e813b42 | ||
|
|
51f36b1903 | ||
|
|
6b122d7262 | ||
|
|
9f1aed3209 | ||
|
|
ce1110bdd4 | ||
|
|
788569a5f4 | ||
|
|
91397eaf57 | ||
|
|
c293ceefcf | ||
|
|
a8e5ce9c13 | ||
|
|
8ac916b0ff | ||
|
|
8d860a051c | ||
|
|
256f63dd38 | ||
|
|
0948964677 | ||
|
|
4ac2c4ac34 | ||
|
|
7c27193983 | ||
|
|
2c3d2ff6a7 | ||
|
|
a4fff01647 | ||
|
|
2cd985faf7 | ||
|
|
9a35302d4b | ||
|
|
553f7675f2 | ||
|
|
3e714ab9f8 | ||
|
|
f2e9a6a502 | ||
|
|
b924e15afa | ||
|
|
b0c74909ba | ||
|
|
548a6b4319 | ||
|
|
b299879b82 | ||
|
|
3696562414 | ||
|
|
e02c6c274b | ||
|
|
9c9afaf78f | ||
|
|
6cde453032 | ||
|
|
8f06889996 | ||
|
|
52662f0766 | ||
|
|
ab0a4b8140 | ||
|
|
1b268ae9db | ||
|
|
8debb9fd11 | ||
|
|
7c07488e16 | ||
|
|
6073a9e2c3 | ||
|
|
9c652f86c9 | ||
|
|
3c0090d86f | ||
|
|
9132755d49 | ||
|
|
2a44691cb3 | ||
|
|
0d8a696498 | ||
|
|
bfa2706598 | ||
|
|
5fdb52388a | ||
|
|
799dc9a1ca | ||
|
|
2bb56e8a4b | ||
|
|
084d2bf692 | ||
|
|
10640c7561 | ||
|
|
9f044c48fe | ||
|
|
5567e1b7f2 | ||
|
|
3cd18d1e16 | ||
|
|
9d3e42b5d4 | ||
|
|
0f318c26c2 | ||
|
|
79f4e69a05 | ||
|
|
f13148af3d | ||
|
|
6598d23ff0 | ||
|
|
c83436655c | ||
|
|
62595c519c | ||
|
|
d2eb2d2941 | ||
|
|
942c0ee113 | ||
|
|
fbd3a38587 | ||
|
|
45b660985e | ||
|
|
0888125899 | ||
|
|
c85d9bcd84 | ||
|
|
dbf8af1146 | ||
|
|
d7ccf1454e | ||
|
|
652d447f8b | ||
|
|
8e91640084 | ||
|
|
0ca2891166 | ||
|
|
5000bb8db3 | ||
|
|
9927424826 | ||
|
|
2f58379feb | ||
|
|
c14d3f4274 | ||
|
|
d4673a2f07 | ||
|
|
3a0c94577f | ||
|
|
5a4e33e503 | ||
|
|
5649799167 | ||
|
|
c407b73c22 | ||
|
|
361add3385 | ||
|
|
9d6ab69d37 | ||
|
|
0f6da35c0b | ||
|
|
ad3f5de99a | ||
|
|
2de7ba0d0c | ||
|
|
b699088dd6 | ||
|
|
458c070004 | ||
|
|
babac6df3c | ||
|
|
f58477931f | ||
|
|
2171d491a6 | ||
|
|
aa911f88f2 | ||
|
|
bdbcaeff67 | ||
|
|
b2756b3c63 | ||
|
|
27f11ab583 | ||
|
|
2776970317 | ||
|
|
9d28bf7e82 | ||
|
|
6455b00742 | ||
|
|
16179a3b50 | ||
|
|
6a37c9d076 | ||
|
|
1915b1c00a | ||
|
|
a9982d6e28 | ||
|
|
1daeb8fe93 | ||
|
|
3dfb158382 | ||
|
|
fb7d247fa7 | ||
|
|
6bf2312a94 | ||
|
|
0cdcb83a7a | ||
|
|
e4f48e81fc | ||
|
|
1d32a95a09 | ||
|
|
4c934a78a6 | ||
|
|
c47bc86d37 | ||
|
|
a125781312 | ||
|
|
dfa951e574 | ||
|
|
76779e6f95 | ||
|
|
e9a79a32da | ||
|
|
967170a7b2 | ||
|
|
3326784315 | ||
|
|
fc553e1009 | ||
|
|
da172ff9b5 | ||
|
|
fc422853ef | ||
|
|
2852c07ec7 | ||
|
|
ead1c9ecab | ||
|
|
5b5066577f | ||
|
|
4af0bb3943 | ||
|
|
f2eaa79318 | ||
|
|
2ee7ce5829 | ||
|
|
0d7c94e7e9 | ||
|
|
9e29821012 | ||
|
|
38c307d6f1 | ||
|
|
520567793a | ||
|
|
e0fb379511 | ||
|
|
ba9362ccb2 | ||
|
|
261a36c435 | ||
|
|
cb92e46f8d | ||
|
|
126186041e | ||
|
|
6379e1703c | ||
|
|
2b246e431b | ||
|
|
526fcabffe | ||
|
|
75ff31f0cf | ||
|
|
46dab6e474 | ||
|
|
c7e8c07d40 | ||
|
|
932d2b77dc | ||
|
|
049de84cbb | ||
|
|
3bd44ea7ef | ||
|
|
317ccccfc6 | ||
|
|
220da6b58e | ||
|
|
6a7750d354 | ||
|
|
529803f791 | ||
|
|
4c23ab5664 | ||
|
|
e3c28fd0ec | ||
|
|
56ab61c29c | ||
|
|
d3056ba843 | ||
|
|
e34e2ec1f1 | ||
|
|
524bb5e4b7 | ||
|
|
3f8ea7764e | ||
|
|
f0d1e6936e | ||
|
|
9a21eec1b9 | ||
|
|
1703346bb6 | ||
|
|
b93d8e73a2 | ||
|
|
17c9813c98 | ||
|
|
e5ebe20a20 | ||
|
|
54a03fd0d3 | ||
|
|
e8affcfde9 | ||
|
|
d376947a91 | ||
|
|
59e38fbdb0 | ||
|
|
492449b7c5 | ||
|
|
7cd21636d6 | ||
|
|
6ff49589be | ||
|
|
c950806541 | ||
|
|
3dcc12042f | ||
|
|
92925648e6 | ||
|
|
811c492bce | ||
|
|
73fa2e19e4 | ||
|
|
921e1af72b | ||
|
|
cc905da630 | ||
|
|
74bbfce8a0 | ||
|
|
8b67a0423d | ||
|
|
f1d527aa9c | ||
|
|
9e45d4d227 | ||
|
|
2dd0424d8f | ||
|
|
838f25b9db | ||
|
|
8809469f8e | ||
|
|
289f138c2a | ||
|
|
3d0dd60f56 | ||
|
|
9bb9a914ac | ||
|
|
44cef9999c | ||
|
|
3a792a021c | ||
|
|
2e5c63cfb9 | ||
|
|
9845363349 | ||
|
|
1a6fa7a799 | ||
|
|
6cd44662a8 | ||
|
|
9daf418886 | ||
|
|
37ee13353d | ||
|
|
8439e8871f | ||
|
|
4c1d3b4f3a | ||
|
|
cd3c66cb14 | ||
|
|
265b0114e4 | ||
|
|
17a63d599d | ||
|
|
d9e87fcd82 | ||
|
|
78c4cb11eb | ||
|
|
6feea75e45 | ||
|
|
2d1f7d0f33 | ||
|
|
841facc853 | ||
|
|
0e60bd3da7 | ||
|
|
5dc7f1ae2f | ||
|
|
6862cb4e58 | ||
|
|
0591530d44 | ||
|
|
592679538b | ||
|
|
9ef2699372 | ||
|
|
e4c37b916a | ||
|
|
7a8a0ae37e | ||
|
|
f6ab59ceda | ||
|
|
f1004e2e36 | ||
|
|
26eaec4c72 | ||
|
|
d0419edb92 | ||
|
|
f972733426 |
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @helloanoop @maintainer-bruno @lohit-bruno @naman-bruno
|
||||
4
.github/ISSUE_TEMPLATE/BugReport.yaml
vendored
4
.github/ISSUE_TEMPLATE/BugReport.yaml
vendored
@@ -27,7 +27,9 @@ body:
|
||||
required: false
|
||||
- label: annoying
|
||||
required: false
|
||||
|
||||
- label: this feature was working in a previous version but is broken in the current release.
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Bruno version
|
||||
|
||||
47
.github/workflows/tests.yml
vendored
47
.github/workflows/tests.yml
vendored
@@ -28,6 +28,11 @@ jobs:
|
||||
npm run build --workspace=packages/bruno-common
|
||||
npm run build --workspace=packages/bruno-query
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build --workspace=packages/bruno-converters
|
||||
npm run build --workspace=packages/bruno-requests
|
||||
|
||||
- name: Lint Check
|
||||
run: npm run lint
|
||||
|
||||
# tests
|
||||
- name: Test Package bruno-js
|
||||
@@ -45,6 +50,8 @@ jobs:
|
||||
run: npm run test --workspace=packages/bruno-app
|
||||
- name: Test Package bruno-common
|
||||
run: npm run test --workspace=packages/bruno-common
|
||||
- name: Test Package bruno-converters
|
||||
run: npm run test --workspace=packages/bruno-converters
|
||||
- name: Test Package bruno-electron
|
||||
run: npm run test --workspace=packages/bruno-electron
|
||||
|
||||
@@ -71,6 +78,8 @@ jobs:
|
||||
npm run build --workspace=packages/bruno-query
|
||||
npm run build --workspace=packages/bruno-common
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build --workspace=packages/bruno-converters
|
||||
npm run build --workspace=packages/bruno-requests
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
@@ -82,5 +91,43 @@ jobs:
|
||||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||
if: always()
|
||||
with:
|
||||
check_name: CLI Test Results
|
||||
files: packages/bruno-tests/collection/junit.xml
|
||||
comment_mode: always
|
||||
e2e-test:
|
||||
name: Playwright E2E Tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: v22.11.x
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get --no-install-recommends install -y \
|
||||
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
|
||||
xvfb
|
||||
npm ci --legacy-peer-deps
|
||||
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
|
||||
- name: Build libraries
|
||||
run: |
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
xvfb-run npm run test:e2e
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -46,4 +46,8 @@ yarn-error.log*
|
||||
|
||||
#dev editor
|
||||
bruno.iml
|
||||
.idea
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# Playwright
|
||||
/blob-report/
|
||||
|
||||
@@ -15,15 +15,15 @@
|
||||
| [正體中文](docs/contributing/contributing_zhtw.md)
|
||||
| [日本語](docs/contributing/contributing_ja.md)
|
||||
| [हिंदी](docs/contributing/contributing_hi.md)
|
||||
| [Nederlands](docs/contributing/contributing_nl.md)
|
||||
| [Dutch](docs/contributing/contributing_nl.md)
|
||||
|
||||
## Let's make Bruno better, together!!
|
||||
|
||||
We are happy that you are looking to improve Bruno. Below are the guidelines to get started bringing up Bruno on your computer.
|
||||
We are happy that you are looking to improve Bruno. Below are the guidelines to run Bruno on your computer.
|
||||
|
||||
### Technology Stack
|
||||
|
||||
Bruno is built using Next.js and React. We also use electron to ship a desktop version (that supports local collections)
|
||||
Bruno is built using React and Electron.
|
||||
|
||||
Libraries we use
|
||||
|
||||
@@ -37,38 +37,76 @@ Libraries we use
|
||||
- Filesystem Watcher - chokidar
|
||||
- i18n - i18next
|
||||
|
||||
### Dependencies
|
||||
|
||||
You would need [Node v20.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
|
||||
> [!IMPORTANT]
|
||||
> You would need [Node v22.x or the latest LTS version](https://nodejs.org/en/). We use npm workspaces in the project
|
||||
|
||||
## Development
|
||||
|
||||
Bruno is being developed as a desktop app. You need to load the app by running the Next.js app in one terminal and then run the electron app in another terminal.
|
||||
Bruno is a desktop app. Below are the instructions to run Bruno.
|
||||
|
||||
### Local Development
|
||||
> Note: We use React for the frontend and rsbuild for build and dev server.
|
||||
|
||||
## Install Dependencies
|
||||
|
||||
```bash
|
||||
# use nodejs 20 version
|
||||
# use nodejs 22 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
npm i --legacy-peer-deps
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
#### Build packages
|
||||
|
||||
##### Option 1
|
||||
|
||||
```bash
|
||||
# build packages
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# bundle js sandbox libraries
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
```
|
||||
##### Option 2
|
||||
|
||||
# run next app (terminal 1)
|
||||
```bash
|
||||
# install dependencies and setup
|
||||
npm run setup
|
||||
```
|
||||
|
||||
#### Run the app
|
||||
|
||||
##### Option 1
|
||||
|
||||
```bash
|
||||
# run react app (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# run electron app (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
##### Option 2
|
||||
```bash
|
||||
# run electron and react app concurrently
|
||||
npm run dev
|
||||
```
|
||||
|
||||
#### Customize Electron `userData` path
|
||||
If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly.
|
||||
|
||||
e.g.
|
||||
```sh
|
||||
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
|
||||
```
|
||||
This will create a `bruno-test` folder on your Desktop and use it as the `userData` path.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
|
||||
@@ -87,7 +125,28 @@ find . -type f -name "package-lock.json" -delete
|
||||
|
||||
```bash
|
||||
# run bruno-schema tests
|
||||
npm test --workspace=packages/bruno-schema
|
||||
npm run test --workspace=packages/bruno-schema
|
||||
|
||||
# run bruno-query tests
|
||||
npm run test --workspace=packages/bruno-query
|
||||
|
||||
# run bruno-common tests
|
||||
npm run test --workspace=packages/bruno-common
|
||||
|
||||
# run bruno-converters tests
|
||||
npm run test --workspace=packages/bruno-converters
|
||||
|
||||
# run bruno-app tests
|
||||
npm run test --workspace=packages/bruno-app
|
||||
|
||||
# run bruno-electron tests
|
||||
npm run test --workspace=packages/bruno-electron
|
||||
|
||||
# run bruno-lang tests
|
||||
npm run test --workspace=packages/bruno-lang
|
||||
|
||||
# run bruno-toml tests
|
||||
npm run test --workspace=packages/bruno-toml
|
||||
|
||||
# run tests over all workspaces
|
||||
npm test --workspaces --if-present
|
||||
|
||||
@@ -21,7 +21,7 @@ Bibliotheken die wir benutzen
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
Du benötigst [Node v20.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
|
||||
Du benötigst [Node v22.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
|
||||
|
||||
### Lass uns coden
|
||||
|
||||
@@ -42,12 +42,12 @@ Bruno wird als Desktop-Anwendung entwickelt. Um die App zu starten, musst Du zue
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- NodeJS v18
|
||||
- NodeJS v22
|
||||
|
||||
### Lokales Entwickeln
|
||||
|
||||
```bash
|
||||
# use nodejs 18 version
|
||||
# use nodejs 22 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
|
||||
@@ -40,6 +40,8 @@ npm i --legacy-peer-deps
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# Next.js ऐप चलाएँ (टर्मिनल 1 पर)
|
||||
npm run dev:web
|
||||
|
||||
@@ -40,6 +40,8 @@ npm i --legacy-peer-deps
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# run next app (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
@@ -40,6 +40,8 @@ npm i --legacy-peer-deps
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# next 앱 실행 (1번 터미널)
|
||||
npm run dev:web
|
||||
|
||||
@@ -40,6 +40,8 @@ npm i --legacy-peer-deps
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# draai next app (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
@@ -42,6 +42,8 @@ npm i --legacy-peer-deps
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# spustite ďalšiu aplikáciu (terminál 1)
|
||||
npm run dev:web
|
||||
|
||||
5
e2e-tests/001-sanity-tests/001-home-screen.spec.ts
Normal file
5
e2e-tests/001-sanity-tests/001-home-screen.spec.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Check if the logo on top left is visible', async ({ page }) => {
|
||||
await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible();
|
||||
});
|
||||
31
e2e-tests/001-sanity-tests/002-create-new-collection.spec.ts
Normal file
31
e2e-tests/001-sanity-tests/002-create-new-collection.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Create new collection and add a simple HTTP request', async ({ page, createTmpDir }) => {
|
||||
await page.getByLabel('Create Collection').click();
|
||||
await page.getByLabel('Name').click();
|
||||
await page.getByLabel('Name').fill('test-collection');
|
||||
await page.getByLabel('Name').press('Tab');
|
||||
await page.getByLabel('Location').fill(await createTmpDir('test-collection'));
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
await page.getByText('test-collection').click();
|
||||
await page.getByLabel('Safe ModeBETA').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('#create-new-tab').getByRole('img').click();
|
||||
await page.getByPlaceholder('Request Name').fill('r1');
|
||||
await page.getByPlaceholder('Request URL').click();
|
||||
await page.getByPlaceholder('Request URL').fill('http://localhost:8081');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.locator('pre').filter({ hasText: 'http://localhost:' }).click();
|
||||
await page.locator('textarea').fill('/ping');
|
||||
await page.locator('#send-request').getByRole('img').nth(2).click();
|
||||
|
||||
await expect(page.getByRole('main')).toContainText('200 OK');
|
||||
|
||||
await page.getByRole('tab', { name: 'GET r1' }).locator('circle').click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await page.getByText('GETr1').click();
|
||||
await page.getByRole('button', { name: 'Clear response' }).click();
|
||||
await page.locator('body').press('ControlOrMeta+Enter');
|
||||
|
||||
await expect(page.getByRole('main')).toContainText('200 OK');
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"maximized": true,
|
||||
"lastOpenedCollections": ["{{projectRoot}}/packages/bruno-tests/collection"]
|
||||
}
|
||||
49
e2e-tests/bruno-testbench/run-testbench-requests.spec.ts
Normal file
49
e2e-tests/bruno-testbench/run-testbench-requests.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test.describe.parallel('Run Testbench Requests', () => {
|
||||
test('Run bruno-testbench in Developer Mode', async ({ pageWithUserData: page }) => {
|
||||
test.setTimeout(2 * 60 * 1000);
|
||||
|
||||
await page.getByText('bruno-testbench').click();
|
||||
await page.getByLabel('Developer Mode(use only if').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('.environment-selector').nth(1).click();
|
||||
await page.locator('.dropdown-item').getByText('Prod').click();
|
||||
await page.locator('.collection-actions').hover();
|
||||
await page.locator('.collection-actions .icon').click();
|
||||
await page.getByText('Run', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Run Collection' }).click();
|
||||
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
|
||||
|
||||
const result = await page.getByText('Total Requests: ').innerText();
|
||||
const [totalRequests, passed, failed, skipped] = result
|
||||
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
|
||||
.slice(1);
|
||||
|
||||
await expect(parseInt(failed)).toBe(0);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1);
|
||||
});
|
||||
|
||||
test.fixme('Run bruno-testbench in Safe Mode', async ({ pageWithUserData: page }) => {
|
||||
test.setTimeout(2 * 60 * 1000);
|
||||
|
||||
await page.getByText('bruno-testbench').click();
|
||||
await page.getByLabel('Safe ModeBETA').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('.environment-selector').nth(1).click();
|
||||
await page.locator('.dropdown-item').getByText('Prod').click();
|
||||
await page.locator('.collection-actions').hover();
|
||||
await page.locator('.collection-actions .icon').click();
|
||||
await page.getByText('Run', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Run Collection' }).click();
|
||||
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
|
||||
|
||||
const result = await page.getByText('Total Requests: ').innerText();
|
||||
const [totalRequests, passed, failed, skipped] = result
|
||||
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
|
||||
.slice(1);
|
||||
|
||||
await expect(parseInt(failed)).toBe(0);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Should verify all support links with correct URL in preference > Support tab', async ({ page }) => {
|
||||
|
||||
// Open Preferences
|
||||
await page.getByLabel('Open Preferences').click();
|
||||
|
||||
// Verify Support tab
|
||||
await page.getByRole('tab', { name: 'Support' }).click();
|
||||
|
||||
const locator_twitter = page.getByRole('link', { name: 'Twitter' });
|
||||
expect(await locator_twitter.getAttribute('href')).toEqual('https://twitter.com/use_bruno');
|
||||
|
||||
const locator_github = page.getByRole('link', { name: 'GitHub', exact: true });
|
||||
expect(await locator_github.getAttribute('href')).toEqual('https://github.com/usebruno/bruno');
|
||||
|
||||
const locator_discord = page.getByRole('link', { name: 'Discord', exact: true });
|
||||
expect(await locator_discord.getAttribute('href')).toEqual('https://discord.com/invite/KgcZUncpjq');
|
||||
|
||||
const locator_reportissues = page.getByRole('link', { name: 'Report Issues', exact: true });
|
||||
expect(await locator_reportissues.getAttribute('href')).toEqual('https://github.com/usebruno/bruno/issues');
|
||||
|
||||
const locator_documentation = page.getByRole('link', { name: 'Documentation', exact: true });
|
||||
expect(await locator_documentation.getAttribute('href')).toEqual('https://docs.usebruno.com');
|
||||
|
||||
|
||||
});
|
||||
41
eslint.config.js
Normal file
41
eslint.config.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// eslint.config.js
|
||||
const { defineConfig } = require("eslint/config");
|
||||
const globals = require("globals");
|
||||
|
||||
module.exports = defineConfig([
|
||||
{
|
||||
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],
|
||||
ignores: ["**/*.config.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.jest,
|
||||
global: false,
|
||||
require: false,
|
||||
Buffer: false,
|
||||
process: false
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-electron/**/*.{js}"],
|
||||
ignores: ["**/*.config.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
}
|
||||
]);
|
||||
5479
package-lock.json
generated
5479
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
@@ -6,24 +6,31 @@
|
||||
"packages/bruno-electron",
|
||||
"packages/bruno-cli",
|
||||
"packages/bruno-common",
|
||||
"packages/bruno-converters",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-query",
|
||||
"packages/bruno-js",
|
||||
"packages/bruno-lang",
|
||||
"packages/bruno-tests",
|
||||
"packages/bruno-toml",
|
||||
"packages/bruno-graphql-docs"
|
||||
"packages/bruno-graphql-docs",
|
||||
"packages/bruno-requests"
|
||||
],
|
||||
"homepage": "https://usebruno.com",
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.14.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^9.26.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"husky": "^8.0.3",
|
||||
"globals": "^16.1.0",
|
||||
"jest": "^29.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"playwright": "^1.51.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
"rimraf": "^6.0.1",
|
||||
@@ -31,13 +38,17 @@
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "node ./scripts/setup.js",
|
||||
"watch:converters": "npm run watch --workspace=packages/bruno-converters",
|
||||
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
|
||||
"dev:watch": "node ./scripts/dev-hot-reload.js",
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
|
||||
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
|
||||
"dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
|
||||
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
|
||||
"build:bruno-requests": "npm run build --workspace=packages/bruno-requests",
|
||||
"build:bruno-converters": "npm run build --workspace=packages/bruno-converters",
|
||||
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
|
||||
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
|
||||
"build:electron": "node ./scripts/build-electron.js",
|
||||
@@ -47,12 +58,18 @@
|
||||
"build:electron:deb": "./scripts/build-electron.sh deb",
|
||||
"build:electron:rpm": "./scripts/build-electron.sh rpm",
|
||||
"build:electron:snap": "./scripts/build-electron.sh snap",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:report": "npx playwright show-report",
|
||||
"watch:common": "npm run watch --workspace=packages/bruno-common",
|
||||
"test:codegen": "node playwright/codegen.ts",
|
||||
"test:e2e": "playwright test",
|
||||
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
|
||||
"prepare": "husky install"
|
||||
"lint": "node --max_old_space_size=4096 $(npx which eslint)"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.29.5"
|
||||
"rollup": "3.29.5",
|
||||
"electron-store": {
|
||||
"conf": {
|
||||
"json-schema-typed": "8.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/bruno-app/babel.config.js
Normal file
9
packages/bruno-app/babel.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@babel/preset-env',
|
||||
['@babel/preset-react', {
|
||||
runtime: 'automatic'
|
||||
}]
|
||||
],
|
||||
plugins: ['babel-plugin-styled-components']
|
||||
};
|
||||
@@ -12,5 +12,14 @@ module.exports = {
|
||||
},
|
||||
clearMocks: true,
|
||||
moduleDirectories: ['node_modules', 'src'],
|
||||
testEnvironment: 'node'
|
||||
testEnvironment: 'jsdom',
|
||||
transform: {
|
||||
'^.+\\.[jt]sx?$': 'babel-jest'
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!(nanoid|xml-formatter)/)'
|
||||
],
|
||||
testMatch: [
|
||||
'<rootDir>/src/**/*.spec.[jt]s?(x)'
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@usebruno/app",
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "rsbuild dev",
|
||||
@@ -11,7 +12,6 @@
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@fontsource/inter": "^5.0.15",
|
||||
"@prantlf/jsonlint": "^16.0.0",
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
@@ -82,19 +82,28 @@
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.27.1",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@rsbuild/core": "^1.1.2",
|
||||
"@rsbuild/plugin-babel": "^1.0.3",
|
||||
"@rsbuild/plugin-node-polyfill": "^1.2.0",
|
||||
"@rsbuild/plugin-react": "^1.0.7",
|
||||
"@rsbuild/plugin-sass": "^1.1.0",
|
||||
"@rsbuild/plugin-styled-components": "1.1.0",
|
||||
"@testing-library/dom": "^9.3.3",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"autoprefixer": "10.4.20",
|
||||
"babel-jest": "^29.7.0",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
|
||||
"babel-plugin-styled-components": "^2.1.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "7.1.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-loader": "^3.0.1",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"mini-css-extract-plugin": "^2.4.5",
|
||||
"postcss": "8.4.47",
|
||||
"style-loader": "^3.3.1",
|
||||
@@ -102,4 +111,4 @@
|
||||
"webpack": "^5.64.4",
|
||||
"webpack-cli": "^4.9.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export default defineConfig({
|
||||
})
|
||||
],
|
||||
source: {
|
||||
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file
|
||||
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file,
|
||||
},
|
||||
html: {
|
||||
title: 'Bruno'
|
||||
@@ -34,6 +34,16 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
ignoreWarnings: [
|
||||
(warning) => warning.message.includes('Critical dependency: the request of a dependency is an expression') && warning?.moduleDescriptor?.name?.includes('flow-parser')
|
||||
],
|
||||
// Add externals configuration to exclude Node.js libraries
|
||||
externals: {
|
||||
// List specific Node.js modules you want to exclude
|
||||
// Format: 'module-name': 'commonjs module-name'
|
||||
'worker_threads': 'commonjs worker_threads',
|
||||
// 'path': 'commonjs path'
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -102,6 +102,13 @@ const StyledWrapper = styled.div`
|
||||
.cm-s-default span.cm-variable {
|
||||
color: #397d13 !important;
|
||||
}
|
||||
|
||||
//matching bracket fix
|
||||
.CodeMirror-matchingbracket {
|
||||
background: #5cc0b48c !important;
|
||||
text-decoration:unset;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -35,6 +35,7 @@ if (!SERVER_RENDERED) {
|
||||
'res.getHeader(name)',
|
||||
'res.getHeaders()',
|
||||
'res.getBody()',
|
||||
'res.setBody(data)',
|
||||
'res.getResponseTime()',
|
||||
'req',
|
||||
'req.url',
|
||||
@@ -57,6 +58,7 @@ if (!SERVER_RENDERED) {
|
||||
'req.getTimeout()',
|
||||
'req.setTimeout(timeout)',
|
||||
'req.getExecutionMode()',
|
||||
'req.getName()',
|
||||
'bru',
|
||||
'bru.cwd()',
|
||||
'bru.getEnvName()',
|
||||
@@ -79,12 +81,14 @@ if (!SERVER_RENDERED) {
|
||||
'bru.getAssertionResults()',
|
||||
'bru.getTestResults()',
|
||||
'bru.sleep(ms)',
|
||||
'bru.getCollectionName()',
|
||||
'bru.getGlobalEnvVar(key)',
|
||||
'bru.setGlobalEnvVar(key, value)',
|
||||
'bru.runner',
|
||||
'bru.runner.setNextRequest(requestName)',
|
||||
'bru.runner.skipRequest()',
|
||||
'bru.runner.stopExecution()'
|
||||
'bru.runner.stopExecution()',
|
||||
'bru.interpolate(str)'
|
||||
];
|
||||
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
|
||||
const cursor = editor.getCursor();
|
||||
@@ -362,7 +366,7 @@ export default class CodeEditor extends React.Component {
|
||||
let variables = getAllVariables(this.props.collection, this.props.item);
|
||||
this.variables = variables;
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, mode);
|
||||
defineCodeMirrorBrunoVariablesMode(variables, mode, false, this.props.enableVariableHighlighting);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
|
||||
@@ -21,12 +21,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -38,12 +38,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -55,12 +55,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -72,12 +72,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -89,12 +89,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -106,12 +106,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -21,8 +21,8 @@ const BasicAuth = ({ collection }) => {
|
||||
mode: 'basic',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: username,
|
||||
password: basicAuth.password
|
||||
username: username || '',
|
||||
password: basicAuth.password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -34,8 +34,8 @@ const BasicAuth = ({ collection }) => {
|
||||
mode: 'basic',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: basicAuth.username,
|
||||
password: password
|
||||
username: basicAuth.username || '',
|
||||
password: password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -21,8 +21,8 @@ const DigestAuth = ({ collection }) => {
|
||||
mode: 'digest',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: username,
|
||||
password: digestAuth.password
|
||||
username: username || '',
|
||||
password: digestAuth.password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -34,8 +34,8 @@ const DigestAuth = ({ collection }) => {
|
||||
mode: 'digest',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: digestAuth.username,
|
||||
password: password
|
||||
username: digestAuth.username || '',
|
||||
password: password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -28,9 +28,9 @@ const NTLMAuth = ({ collection }) => {
|
||||
mode: 'ntlm',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: username,
|
||||
password: ntlmAuth.password,
|
||||
domain: ntlmAuth.domain
|
||||
username: username || '',
|
||||
password: ntlmAuth.password || '',
|
||||
domain: ntlmAuth.domain || ''
|
||||
|
||||
}
|
||||
})
|
||||
@@ -43,9 +43,9 @@ const NTLMAuth = ({ collection }) => {
|
||||
mode: 'ntlm',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: ntlmAuth.username,
|
||||
password: password,
|
||||
domain: ntlmAuth.domain
|
||||
username: ntlmAuth.username || '',
|
||||
password: password || '',
|
||||
domain: ntlmAuth.domain || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -57,9 +57,9 @@ const NTLMAuth = ({ collection }) => {
|
||||
mode: 'ntlm',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: ntlmAuth.username,
|
||||
password: ntlmAuth.password,
|
||||
domain: domain
|
||||
username: ntlmAuth.username || '',
|
||||
password: ntlmAuth.password || '',
|
||||
domain: domain || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -21,8 +21,8 @@ const WsseAuth = ({ collection }) => {
|
||||
mode: 'wsse',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username,
|
||||
password: wsseAuth.password
|
||||
username: username || '',
|
||||
password: wsseAuth.password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -34,8 +34,8 @@ const WsseAuth = ({ collection }) => {
|
||||
mode: 'wsse',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: wsseAuth.username,
|
||||
password
|
||||
username: wsseAuth.username || '',
|
||||
password: password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -72,7 +72,7 @@ const Info = ({ collection }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showShareCollectionModal && <ShareCollection collection={collection} onClose={handleToggleShowShareCollectionModal(false)} />}
|
||||
{showShareCollectionModal && <ShareCollection collectionUid={collection.uid} onClose={handleToggleShowShareCollectionModal(false)} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
const requestVars = get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = get(collection, 'root.request.vars.res', []);
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
const auth = get(collection, 'root.request.auth', {}).mode;
|
||||
const authMode = get(collection, 'root.request.auth', {}).mode || 'none';
|
||||
|
||||
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
||||
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
@@ -155,7 +155,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
{auth !== 'none' && <ContentIndicator />}
|
||||
{authMode !== 'none' && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => setTab('script')}>
|
||||
Script
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -13,11 +13,18 @@ import { variableNameRegex } from 'utils/common/regex';
|
||||
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections';
|
||||
|
||||
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const addButtonRef = useRef(null);
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
|
||||
let _collection = cloneDeep(collection);
|
||||
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
@@ -160,7 +167,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<SingleLineEditor
|
||||
theme={storedTheme}
|
||||
collection={collection}
|
||||
collection={_collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
isSecret={variable.secret}
|
||||
|
||||
@@ -28,7 +28,10 @@ const ImportEnvironment = ({ collection, onClose }) => {
|
||||
.then(() => {
|
||||
toast.success('Environment imported successfully');
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while importing the environment'));
|
||||
.catch((error) => {
|
||||
toast.error('An error occurred while importing the environment');
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
@@ -11,6 +11,12 @@ const Wrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
.inherit-mode-text {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
.auth-mode-label {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -9,6 +9,14 @@ import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/Passwo
|
||||
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
|
||||
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
|
||||
import AuthMode from '../AuthMode';
|
||||
import BasicAuth from 'components/RequestPane/Auth/BasicAuth';
|
||||
import BearerAuth from 'components/RequestPane/Auth/BearerAuth';
|
||||
import DigestAuth from 'components/RequestPane/Auth/DigestAuth';
|
||||
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 { findItemInCollection, findParentItemInCollection, humanizeRequestAuthMode } from 'utils/collections/index';
|
||||
|
||||
const GrantTypeComponentMap = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -37,12 +45,132 @@ 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;
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: collectionAuth
|
||||
};
|
||||
|
||||
// Get path from collection to current folder
|
||||
const folderTreePath = getTreePathFromCollectionToFolder(collection, folder);
|
||||
|
||||
// Check parent folders to find closest auth configuration
|
||||
// Skip the last item which is the current folder
|
||||
for (let i = 0; i < folderTreePath.length - 1; i++) {
|
||||
const parentFolder = folderTreePath[i];
|
||||
if (parentFolder.type === 'folder') {
|
||||
const folderAuth = get(parentFolder, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
name: parentFolder.name,
|
||||
auth: folderAuth
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return effectiveSource;
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'basic': {
|
||||
return (
|
||||
<BasicAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'bearer': {
|
||||
return (
|
||||
<BearerAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'digest': {
|
||||
return (
|
||||
<DigestAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'ntlm': {
|
||||
return (
|
||||
<NTLMAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'wsse': {
|
||||
return (
|
||||
<WsseAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'apikey': {
|
||||
return (
|
||||
<ApiKeyAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'awsv4': {
|
||||
return (
|
||||
<AwsV4Auth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'oauth2': {
|
||||
return (
|
||||
<>
|
||||
@@ -56,6 +184,17 @@ const Auth = ({ collection, folder }) => {
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'inherit': {
|
||||
const source = getEffectiveAuthSource();
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'none': {
|
||||
return null;
|
||||
}
|
||||
@@ -64,6 +203,7 @@ const Auth = ({ collection, folder }) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
|
||||
@@ -35,6 +35,51 @@ const AuthMode = ({ collection, folder }) => {
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('awsv4');
|
||||
}}
|
||||
>
|
||||
AWS Sig v4
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('basic');
|
||||
}}
|
||||
>
|
||||
Basic Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('bearer');
|
||||
}}
|
||||
>
|
||||
Bearer Token
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('digest');
|
||||
}}
|
||||
>
|
||||
Digest Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('ntlm');
|
||||
}}
|
||||
>
|
||||
NTLM Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
@@ -44,6 +89,33 @@ const AuthMode = ({ collection, folder }) => {
|
||||
>
|
||||
OAuth 2.0
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('wsse');
|
||||
}}
|
||||
>
|
||||
WSSE Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('apikey');
|
||||
}}
|
||||
>
|
||||
API Key
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('inherit');
|
||||
}}
|
||||
>
|
||||
Inherit
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
|
||||
@@ -28,7 +28,7 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
tab = folderLevelSettingsSelectedTab[folder?.uid];
|
||||
}
|
||||
|
||||
const folderRoot = collection?.items.find((item) => item.uid === folder?.uid)?.root;
|
||||
const folderRoot = folder?.root;
|
||||
const hasScripts = folderRoot?.request?.script?.res || folderRoot?.request?.script?.req;
|
||||
const hasTests = folderRoot?.request?.tests;
|
||||
|
||||
|
||||
@@ -34,7 +34,10 @@ const ImportEnvironment = ({ onClose }) => {
|
||||
.then(() => {
|
||||
toast.success('Global Environment imported successfully');
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while importing the environment'));
|
||||
.catch((error) => {
|
||||
toast.error('An error occurred while importing the environment');
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
@@ -8,7 +8,7 @@ const DotIcon = ({ width }) => {
|
||||
className='inline-block'
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor" />
|
||||
<path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" strokeWidth="0" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -130,7 +130,7 @@ class MultiLineEditor extends Component {
|
||||
|
||||
addOverlay = (variables) => {
|
||||
this.variables = variables;
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain');
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', false, true);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ const General = ({ close }) => {
|
||||
className="mousetrap mr-0"
|
||||
/>
|
||||
<label className="block ml-2 select-none" htmlFor="customCaCertificateEnabled">
|
||||
Use custom CA Certificate
|
||||
Use Custom CA Certificate
|
||||
</label>
|
||||
</div>
|
||||
{formik.values.customCaCertificate.filePath ? (
|
||||
@@ -183,7 +183,7 @@ const General = ({ close }) => {
|
||||
className={`block ml-2 select-none ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}
|
||||
htmlFor="keepDefaultCaCertificatesEnabled"
|
||||
>
|
||||
Keep default CA Certificates
|
||||
Keep Default CA Certificates
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
|
||||
@@ -5,21 +5,23 @@ import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAPIKeyPlacement } from 'utils/collections';
|
||||
|
||||
const ApiKeyAuth = ({ item, collection }) => {
|
||||
const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const apikeyAuth = item.draft ? get(item, 'draft.request.auth.apikey', {}) : get(item, 'request.auth.apikey', {});
|
||||
const apikeyAuth = get(request, 'auth.apikey', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
@@ -90,7 +92,7 @@ const ApiKeyAuth = ({ item, collection }) => {
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
dropdownTippyRef?.current?.hide();
|
||||
handleAuthChange('placement', 'header');
|
||||
}}
|
||||
>
|
||||
@@ -99,11 +101,11 @@ const ApiKeyAuth = ({ item, collection }) => {
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
dropdownTippyRef?.current?.hide();
|
||||
handleAuthChange('placement', 'queryparams');
|
||||
}}
|
||||
>
|
||||
Query Params
|
||||
Query Param
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@@ -8,14 +8,17 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { update } from 'lodash';
|
||||
|
||||
const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const awsv4Auth = item.draft ? get(item, 'draft.request.auth.awsv4', {}) : get(item, 'request.auth.awsv4', {});
|
||||
const awsv4Auth = get(request, 'auth.awsv4', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleAccessKeyIdChange = (accessKeyId) => {
|
||||
dispatch(
|
||||
|
||||
@@ -7,14 +7,17 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BasicAuth = ({ item, collection }) => {
|
||||
const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const basicAuth = item.draft ? get(item, 'draft.request.auth.basic', {}) : get(item, 'request.auth.basic', {});
|
||||
const basicAuth = get(request, 'auth.basic', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
|
||||
@@ -7,16 +7,18 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BearerAuth = ({ item, collection }) => {
|
||||
const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const bearerToken = item.draft
|
||||
? get(item, 'draft.request.auth.bearer.token', '')
|
||||
: get(item, 'request.auth.bearer.token', '');
|
||||
// Use the request prop directly like OAuth2ClientCredentials does
|
||||
const bearerToken = get(request, 'auth.bearer.token', '');
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleTokenChange = (token) => {
|
||||
dispatch(
|
||||
|
||||
@@ -3,18 +3,20 @@ import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DigestAuth = ({ item, collection }) => {
|
||||
const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const digestAuth = item.draft ? get(item, 'draft.request.auth.digest', {}) : get(item, 'request.auth.digest', {});
|
||||
const digestAuth = get(request, 'auth.digest', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
|
||||
@@ -7,14 +7,17 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const NTLMAuth = ({ item, collection }) => {
|
||||
const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const ntlmAuth = item.draft ? get(item, 'draft.request.auth.ntlm', {}) : get(item, 'request.auth.ntlm', {});
|
||||
const ntlmAuth = get(request, 'auth.ntlm', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
@@ -26,7 +29,6 @@ const NTLMAuth = ({ item, collection }) => {
|
||||
username: username,
|
||||
password: ntlmAuth.password,
|
||||
domain: ntlmAuth.domain
|
||||
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -239,17 +239,6 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full mb-4">
|
||||
<label className="block min-w-[140px]">Auto-refresh token</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="cursor-pointer w-4 h-4 accent-indigo-600"
|
||||
checked={get(request, 'auth.oauth2.autoRefreshToken', false)}
|
||||
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">Automatically refresh the token when it expires</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 mt-4">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
|
||||
@@ -3,8 +3,7 @@ import { useDispatch } from "react-redux";
|
||||
import toast from 'react-hot-toast';
|
||||
import { cloneDeep, find } from 'lodash';
|
||||
import { IconLoader2 } from '@tabler/icons';
|
||||
import brunoCommon from '@usebruno/common';
|
||||
const { interpolate } = brunoCommon;
|
||||
import { interpolate } from '@usebruno/common';
|
||||
import { fetchOauth2Credentials, clearOauth2Cache, refreshOauth2Credentials } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { getAllVariables } from "utils/collections/index";
|
||||
|
||||
@@ -35,14 +34,14 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
|
||||
toast.success('token fetched successfully!');
|
||||
}
|
||||
else {
|
||||
toast.error('An error occured while fetching token!');
|
||||
toast.error('An error occurred while fetching token!');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('could not fetch the token!');
|
||||
console.error(error);
|
||||
toggleFetchingToken(false);
|
||||
toast.error('An error occured while fetching token!');
|
||||
toast.error('An error occurred while fetching token!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,13 +57,13 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
|
||||
toast.success('token refreshed successfully!');
|
||||
}
|
||||
else {
|
||||
toast.error('An error occured while refreshing token!');
|
||||
toast.error('An error occurred while refreshing token!');
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
toggleRefreshingToken(false);
|
||||
toast.error('An error occured while refreshing token!');
|
||||
toast.error('An error occurred while refreshing token!');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { find } from "lodash";
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
import { useState, useEffect } from "react";
|
||||
import { IconChevronDown, IconChevronRight, IconCopy, IconCheck } from '@tabler/icons';
|
||||
import { getAllVariables } from 'utils/collections/index';
|
||||
import brunoCommon from '@usebruno/common';
|
||||
const { interpolate } = brunoCommon;
|
||||
import { interpolate } from '@usebruno/common';
|
||||
|
||||
const TokenSection = ({ title, token }) => {
|
||||
if (!token) return null;
|
||||
|
||||
@@ -242,17 +242,6 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full mb-4">
|
||||
<label className="block min-w-[140px]">Auto-refresh token</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="cursor-pointer w-4 h-4 accent-indigo-600"
|
||||
checked={get(request, 'auth.oauth2.autoRefreshToken', false)}
|
||||
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">Automatically refresh the token when it expires</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 mt-4">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
|
||||
@@ -7,14 +7,17 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WsseAuth = ({ item, collection }) => {
|
||||
const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const wsseAuth = item.draft ? get(item, 'draft.request.auth.wsse', {}) : get(item, 'request.auth.wsse', {});
|
||||
const wsseAuth = get(request, 'auth.wsse', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleUserChange = (username) => {
|
||||
dispatch(
|
||||
@@ -55,6 +58,7 @@ const WsseAuth = ({ item, collection }) => {
|
||||
onChange={(val) => handleUserChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -67,6 +71,8 @@ const WsseAuth = ({ item, collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -7,6 +7,8 @@ import BasicAuth from './BasicAuth';
|
||||
import DigestAuth from './DigestAuth';
|
||||
import WsseAuth from './WsseAuth';
|
||||
import NTLMAuth from './NTLMAuth';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import ApiKeyAuth from './ApiKeyAuth';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -27,6 +29,16 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
const Auth = ({ item, collection }) => {
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
|
||||
// Create a request object to pass to the auth components
|
||||
const request = item.draft
|
||||
? get(item, 'draft.request', {})
|
||||
: get(item, 'request', {});
|
||||
|
||||
// Save function for request level
|
||||
const save = () => {
|
||||
return saveRequest(item.uid, collection.uid);
|
||||
};
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
@@ -59,28 +71,28 @@ const Auth = ({ item, collection }) => {
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'awsv4': {
|
||||
return <AwsV4Auth collection={collection} item={item} />;
|
||||
return <AwsV4Auth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'basic': {
|
||||
return <BasicAuth collection={collection} item={item} />;
|
||||
return <BasicAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'bearer': {
|
||||
return <BearerAuth collection={collection} item={item} />;
|
||||
return <BearerAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'digest': {
|
||||
return <DigestAuth collection={collection} item={item} />;
|
||||
return <DigestAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'ntlm': {
|
||||
return <NTLMAuth collection={collection} item={item} />;
|
||||
return <NTLMAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'oauth2': {
|
||||
return <OAuth2 collection={collection} item={item} />;
|
||||
return <OAuth2 collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'wsse': {
|
||||
return <WsseAuth collection={collection} item={item} />;
|
||||
return <WsseAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'apikey': {
|
||||
return <ApiKeyAuth collection={collection} item={item} />;
|
||||
return <ApiKeyAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'inherit': {
|
||||
const source = getEffectiveAuthSource();
|
||||
|
||||
@@ -7,8 +7,10 @@ import Dropdown from '../../Dropdown';
|
||||
|
||||
const GraphQLSchemaActions = ({ item, collection, onSchemaLoad, toggleDocs }) => {
|
||||
const url = item.draft ? get(item, 'draft.request.url', '') : get(item, 'request.url', '');
|
||||
const pathname = item.draft ? get(item, 'draft.pathname', '') : get(item, 'pathname', '');
|
||||
const uid = item.draft ? get(item, 'draft.uid', '') : get(item, 'uid', '');
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
const request = item.draft ? { ...item.draft.request, pathname, uid } : { ...item.request, pathname, uid };
|
||||
|
||||
let {
|
||||
schema,
|
||||
|
||||
@@ -64,9 +64,10 @@ const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
onEdit={onEdit}
|
||||
mode="javascript"
|
||||
mode="application/json"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -140,7 +140,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
</div>
|
||||
</div>
|
||||
{generateCodeItemModalOpen && (
|
||||
<GenerateCodeItem collection={collection} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
|
||||
<GenerateCodeItem collectionUid={collection.uid} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ const RequestBody = ({ item, collection }) => {
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
item={item}
|
||||
item={item}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
@@ -58,13 +58,14 @@ const RequestBody = ({ item, collection }) => {
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
mode={codeMirrorMode[bodyMode]}
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (bodyMode === 'file') {
|
||||
return <FileBody item={item} collection={collection}/>
|
||||
return <FileBody item={item} collection={collection} />;
|
||||
}
|
||||
|
||||
if (bodyMode === 'formUrlEncoded') {
|
||||
@@ -77,4 +78,4 @@ const RequestBody = ({ item, collection }) => {
|
||||
|
||||
return <StyledWrapper className="w-full">No Body</StyledWrapper>;
|
||||
};
|
||||
export default RequestBody;
|
||||
export default RequestBody;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const FolderNotFound = ({ folderUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [showErrorMessage, setShowErrorMessage] = useState(false);
|
||||
|
||||
const closeTab = useCallback(() => {
|
||||
dispatch(
|
||||
closeTabs({
|
||||
tabUids: [folderUid]
|
||||
})
|
||||
);
|
||||
}, [dispatch, folderUid]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setShowErrorMessage(true);
|
||||
}, 300);
|
||||
}, []);
|
||||
|
||||
if (!showErrorMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6 px-6">
|
||||
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700">
|
||||
<div>Folder no longer exists.</div>
|
||||
<div className="mt-2">
|
||||
This can happen when the folder was renamed or deleted on your filesystem.
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-md btn-secondary mt-6" onClick={closeTab}>
|
||||
Close Tab
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FolderNotFound;
|
||||
@@ -25,6 +25,7 @@ import { produce } from 'immer';
|
||||
import CollectionOverview from 'components/CollectionSettings/Overview';
|
||||
import RequestNotLoaded from './RequestNotLoaded';
|
||||
import RequestIsLoading from './RequestIsLoading';
|
||||
import FolderNotFound from './FolderNotFound';
|
||||
|
||||
const MIN_LEFT_PANE_WIDTH = 300;
|
||||
const MIN_RIGHT_PANE_WIDTH = 350;
|
||||
@@ -163,6 +164,10 @@ const RequestTabPanel = () => {
|
||||
|
||||
if (focusedTab.type === 'folder-settings') {
|
||||
const folder = findItemInCollection(collection, focusedTab.folderUid);
|
||||
if (!folder) {
|
||||
return <FolderNotFound folderUid={focusedTab.folderUid} />;
|
||||
}
|
||||
|
||||
return <FolderSettings collection={collection} folder={folder} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,9 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
className={`flex items-center justify-between tab-container px-1 ${tab.preview ? "italic" : ""}`}
|
||||
onMouseUp={handleMouseUp} // Add middle-click behavior here
|
||||
>
|
||||
{tab.type === 'folder-settings' ? (
|
||||
{tab.type === 'folder-settings' && !folder ? (
|
||||
<RequestTabNotFound handleCloseClick={handleCloseClick} />
|
||||
) : tab.type === 'folder-settings' ? (
|
||||
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} />
|
||||
) : (
|
||||
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} />
|
||||
@@ -261,13 +263,13 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
|
||||
return (
|
||||
<Fragment>
|
||||
{showAddNewRequestModal && (
|
||||
<NewRequest collection={collection} onClose={() => setShowAddNewRequestModal(false)} />
|
||||
<NewRequest collectionUid={collection.uid} onClose={() => setShowAddNewRequestModal(false)} />
|
||||
)}
|
||||
|
||||
{showCloneRequestModal && (
|
||||
<CloneCollectionItem
|
||||
item={currentTabItem}
|
||||
collection={collection}
|
||||
collectionUid={collection.uid}
|
||||
onClose={() => setShowCloneRequestModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -79,7 +79,7 @@ const RequestTabs = () => {
|
||||
return (
|
||||
<StyledWrapper className={getRootClassname()}>
|
||||
{newRequestModalOpen && (
|
||||
<NewRequest collection={activeCollection} onClose={() => setNewRequestModalOpen(false)} />
|
||||
<NewRequest collectionUid={activeCollection?.uid} onClose={() => setNewRequestModalOpen(false)} />
|
||||
)}
|
||||
{collectionRequestTabs && collectionRequestTabs.length ? (
|
||||
<>
|
||||
|
||||
@@ -17,7 +17,7 @@ const ResponseLoadingOverlay = ({ item, collection }) => {
|
||||
<div className="overlay">
|
||||
<div style={{ marginBottom: 15, fontSize: 26 }}>
|
||||
<div style={{ display: 'inline-block', fontSize: 20, marginLeft: 5, marginRight: 5 }}>
|
||||
<StopWatch requestTimestamp={item?.requestSent?.timestamp} />
|
||||
<StopWatch startTime={item?.requestStartTime} />
|
||||
</div>
|
||||
</div>
|
||||
<IconRefresh size={24} className="loading-icon" />
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import ResponseSize from './index';
|
||||
|
||||
// Create minimal theme with only the properties needed for the component
|
||||
const theme = {
|
||||
requestTabPanel: {
|
||||
responseStatus: '#666'
|
||||
}
|
||||
};
|
||||
|
||||
// Wrap component with theme provider for styled-components
|
||||
const renderWithTheme = (component) => {
|
||||
return render(
|
||||
<ThemeProvider theme={theme}>
|
||||
{component}
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('ResponseSize', () => {
|
||||
describe('Invalid or excluded size values', () => {
|
||||
it('should not render when size is undefined', () => {
|
||||
const { container } = renderWithTheme(<ResponseSize size={undefined} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should not render when size is null', () => {
|
||||
const { container } = renderWithTheme(<ResponseSize size={null} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should not render when size is NaN', () => {
|
||||
const { container } = renderWithTheme(<ResponseSize size={NaN} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should not render when size is Infinity', () => {
|
||||
const { container } = renderWithTheme(<ResponseSize size={Infinity} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should not render when size is -Infinity', () => {
|
||||
const { container } = renderWithTheme(<ResponseSize size={-Infinity} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should not render when size is a string', () => {
|
||||
const { container } = renderWithTheme(<ResponseSize size="1024" />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should not render when size is an object', () => {
|
||||
const { container } = renderWithTheme(<ResponseSize size={{value: 1024}} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Valid size values', () => {
|
||||
it('should handle zero bytes', () => {
|
||||
renderWithTheme(<ResponseSize size={0} />);
|
||||
const element = screen.getByText(/0B/);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.textContent).toMatch(/^0B$/);
|
||||
expect(element).toHaveAttribute('title', '0B');
|
||||
});
|
||||
|
||||
it('should render bytes when size is less than 1024', () => {
|
||||
renderWithTheme(<ResponseSize size={500} />);
|
||||
const element = screen.getByText(/500B/);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.textContent).toMatch(/^500B$/);
|
||||
expect(element).toHaveAttribute('title', '500B');
|
||||
});
|
||||
|
||||
it('should handle exactly 1024 bytes as size', () => {
|
||||
renderWithTheme(<ResponseSize size={1024} />);
|
||||
const element = screen.getByText(/1024B/);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.textContent).toMatch(/^1024B$/);
|
||||
expect(element).toHaveAttribute('title', '1,024B');
|
||||
});
|
||||
|
||||
it('should render kilobytes when size is greater than 1024', () => {
|
||||
renderWithTheme(<ResponseSize size={1500} />);
|
||||
const element = screen.getByText(/1\.46KB/);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.textContent).toMatch(/^\d+\.\d+KB$/);
|
||||
expect(element).toHaveAttribute('title', '1,500B');
|
||||
});
|
||||
|
||||
it('should handle large size numbers', () => {
|
||||
renderWithTheme(<ResponseSize size={10240} />);
|
||||
const element = screen.getByText(/10\.0KB/);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.textContent).toMatch(/^\d+\.\d+KB$/);
|
||||
expect(element).toHaveAttribute('title', '10,240B');
|
||||
});
|
||||
|
||||
it('should handle decimal size numbers', () => {
|
||||
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', '1,126.5B');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,14 +2,20 @@ import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ResponseSize = ({ size }) => {
|
||||
|
||||
if (!Number.isFinite(size)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let sizeToDisplay = '';
|
||||
|
||||
// If size is greater than 1024 bytes, format as KB
|
||||
if (size > 1024) {
|
||||
// size is greater than 1kb
|
||||
let kb = Math.floor(size / 1024);
|
||||
let decimal = Math.round(((size % 1024) / 1024).toFixed(2) * 100);
|
||||
sizeToDisplay = kb + '.' + decimal + 'KB';
|
||||
} else {
|
||||
// If size is less than or equal to 1024 bytes, display as bytes (B)
|
||||
sizeToDisplay = size + 'B';
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
padding-top: 20%;
|
||||
width: 100%;
|
||||
.send-icon {
|
||||
color: ${(props) => props.theme.requestTabPanel.responseSendIcon};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { IconCircleOff } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const SkippedRequest = () => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="send-icon flex justify-center" style={{ fontSize: 200 }}>
|
||||
<IconCircleOff size={150} strokeWidth={1} />
|
||||
</div>
|
||||
<div className="flex mt-4 justify-center" style={{ fontSize: 25 }}>
|
||||
Request skipped
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkippedRequest;
|
||||
@@ -1,6 +1,18 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
.test-summary {
|
||||
transition: background-color 0.2s;
|
||||
border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.indentBorder};
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
.test-success {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
@@ -9,9 +21,25 @@ const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
.test-success-count {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
.test-failure-count {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.test-results-list {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,63 +1,151 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconCircleCheck,
|
||||
IconCircleX
|
||||
} from '@tabler/icons';
|
||||
|
||||
const TestResults = ({ results, assertionResults }) => {
|
||||
const ResultIcon = ({ status }) => (
|
||||
<span className={`inline-flex items-center ${status === 'pass' ? 'test-success' : 'test-failure'}`}>
|
||||
{status === 'pass' ? (
|
||||
<IconCircleCheck size={14} className="mr-1" aria-label="Test passed" />
|
||||
) : (
|
||||
<IconCircleX size={14} className="mr-1" aria-label="Test failed" />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
const ErrorMessage = ({ error }) => error && (
|
||||
<>
|
||||
<br />
|
||||
<span className="error-message pl-8" role="alert">
|
||||
{error}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
const ResultItem = ({ result, type }) => (
|
||||
<div className="test-result-item">
|
||||
<ResultIcon status={result.status} />
|
||||
<span className={result.status === 'pass' ? 'test-success' : 'test-failure'}>
|
||||
{type === 'assertion'
|
||||
? `${result.lhsExpr}: ${result.rhsExpr}`
|
||||
: result.description
|
||||
}
|
||||
</span>
|
||||
<ErrorMessage error={result.error} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const TestSection = ({
|
||||
title,
|
||||
results,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
type = 'test'
|
||||
}) => {
|
||||
const passedResults = results.filter((result) => result.status === 'pass');
|
||||
const failedResults = results.filter((result) => result.status === 'fail');
|
||||
|
||||
if (results.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className='mb-4'>
|
||||
<div
|
||||
className="font-medium test-summary flex items-center cursor-pointer hover:bg-opacity-10 hover:bg-gray-500 rounded py-2"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<span className="dropdown-icon mr-2 flex items-center">
|
||||
{isExpanded ?
|
||||
<IconChevronDown size={18} stroke={1.5} /> :
|
||||
<IconChevronRight size={18} stroke={1.5} />
|
||||
}
|
||||
</span>
|
||||
<span className="flex-grow">
|
||||
{title} ({results.length}), Passed: {passedResults.length}, Failed: {failedResults.length}
|
||||
</span>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<ul className="ml-5">
|
||||
{results.map((result) => (
|
||||
<li key={result.uid} className="py-1">
|
||||
<ResultItem result={result} type={type} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TestResults = ({ results, assertionResults, preRequestTestResults, postResponseTestResults }) => {
|
||||
results = results || [];
|
||||
assertionResults = assertionResults || [];
|
||||
if (!results.length && !assertionResults.length) {
|
||||
preRequestTestResults = preRequestTestResults || [];
|
||||
postResponseTestResults = postResponseTestResults || [];
|
||||
|
||||
const [expandedSections, setExpandedSections] = useState({
|
||||
preRequest: true,
|
||||
tests: true,
|
||||
postResponse: true,
|
||||
assertions: true
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedSections({
|
||||
preRequest: preRequestTestResults.length > 0,
|
||||
tests: results.length > 0,
|
||||
postResponse: postResponseTestResults.length > 0,
|
||||
assertions: assertionResults.length > 0
|
||||
});
|
||||
}, [results.length, assertionResults.length, preRequestTestResults.length, postResponseTestResults.length]);
|
||||
|
||||
const toggleSection = (section) => {
|
||||
setExpandedSections({
|
||||
...expandedSections,
|
||||
[section]: !expandedSections[section]
|
||||
});
|
||||
};
|
||||
|
||||
if (!results.length && !assertionResults.length && !preRequestTestResults.length && !postResponseTestResults.length) {
|
||||
return <div className="px-3">No tests found</div>;
|
||||
}
|
||||
|
||||
const passedTests = results.filter((result) => result.status === 'pass');
|
||||
const failedTests = results.filter((result) => result.status === 'fail');
|
||||
|
||||
const passedAssertions = assertionResults.filter((result) => result.status === 'pass');
|
||||
const failedAssertions = assertionResults.filter((result) => result.status === 'fail');
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col">
|
||||
<div className="pb-2 font-medium test-summary">
|
||||
Tests ({results.length}/{results.length}), Passed: {passedTests.length}, Failed: {failedTests.length}
|
||||
</div>
|
||||
<ul className="">
|
||||
{results.map((result) => (
|
||||
<li key={result.uid} className="py-1">
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success">✔ {result.description}</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure">✘ {result.description}</span>
|
||||
<br />
|
||||
<span className="error-message pl-8">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<StyledWrapper className="flex flex-col px-3">
|
||||
<TestSection
|
||||
title="Pre-Request Tests"
|
||||
results={preRequestTestResults}
|
||||
isExpanded={expandedSections.preRequest}
|
||||
onToggle={() => toggleSection('preRequest')}
|
||||
type="test"
|
||||
/>
|
||||
|
||||
<div className="py-2 font-medium test-summary">
|
||||
Assertions ({assertionResults.length}/{assertionResults.length}), Passed: {passedAssertions.length}, Failed:{' '}
|
||||
{failedAssertions.length}
|
||||
</div>
|
||||
<ul className="">
|
||||
{assertionResults.map((result) => (
|
||||
<li key={result.uid} className="py-1">
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success">
|
||||
✔ {result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure">
|
||||
✘ {result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
<br />
|
||||
<span className="error-message pl-8">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<TestSection
|
||||
title="Post-Response Tests"
|
||||
results={postResponseTestResults}
|
||||
isExpanded={expandedSections.postResponse}
|
||||
onToggle={() => toggleSection('postResponse')}
|
||||
type="test"
|
||||
/>
|
||||
|
||||
<TestSection
|
||||
title="Tests"
|
||||
results={results}
|
||||
isExpanded={expandedSections.tests}
|
||||
onToggle={() => toggleSection('tests')}
|
||||
type="test"
|
||||
/>
|
||||
|
||||
<TestSection
|
||||
title="Assertions"
|
||||
results={assertionResults}
|
||||
isExpanded={expandedSections.assertions}
|
||||
onToggle={() => toggleSection('assertions')}
|
||||
type="assertion"
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import React from 'react';
|
||||
import { IconCircleCheck, IconCircleX } from '@tabler/icons';
|
||||
|
||||
const TestResultsLabel = ({ results, assertionResults }) => {
|
||||
const TestResultsLabel = ({ results, assertionResults, preRequestTestResults, postResponseTestResults }) => {
|
||||
results = results || [];
|
||||
assertionResults = assertionResults || [];
|
||||
if (!results.length && !assertionResults.length) {
|
||||
preRequestTestResults = preRequestTestResults || [];
|
||||
postResponseTestResults = postResponseTestResults || [];
|
||||
|
||||
if (!results.length && !assertionResults.length && !preRequestTestResults.length && !postResponseTestResults.length) {
|
||||
return 'Tests';
|
||||
}
|
||||
|
||||
@@ -13,8 +17,14 @@ const TestResultsLabel = ({ results, assertionResults }) => {
|
||||
const numberOfAssertions = assertionResults.length;
|
||||
const numberOfFailedAssertions = assertionResults.filter((result) => result.status === 'fail').length;
|
||||
|
||||
const totalNumberOfTests = numberOfTests + numberOfAssertions;
|
||||
const totalNumberOfFailedTests = numberOfFailedTests + numberOfFailedAssertions;
|
||||
const numberOfPreRequestTests = preRequestTestResults.length;
|
||||
const numberOfFailedPreRequestTests = preRequestTestResults.filter((result) => result.status === 'fail').length;
|
||||
|
||||
const numberOfPostResponseTests = postResponseTestResults.length;
|
||||
const numberOfFailedPostResponseTests = postResponseTestResults.filter((result) => result.status === 'fail').length;
|
||||
|
||||
const totalNumberOfTests = numberOfTests + numberOfAssertions + numberOfPreRequestTests + numberOfPostResponseTests;
|
||||
const totalNumberOfFailedTests = numberOfFailedTests + numberOfFailedAssertions + numberOfFailedPreRequestTests + numberOfFailedPostResponseTests;
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -2,9 +2,17 @@ const Network = ({ logs }) => {
|
||||
return (
|
||||
<div className="bg-black/5 text-white network-logs rounded overflow-auto h-96">
|
||||
<pre className="whitespace-pre-wrap">
|
||||
{logs.map((entry, index) => (
|
||||
<NetworkLogsEntry key={index} entry={entry} />
|
||||
))}
|
||||
{logs.map((currentLog, index) => {
|
||||
if (index > 0 && currentLog?.type === 'separator') {
|
||||
return <div className="border-t-2 border-gray-500 w-full my-2" key={index} />;
|
||||
}
|
||||
const nextLog = logs[index + 1];
|
||||
const isSameLogType = nextLog?.type === currentLog?.type;
|
||||
return <>
|
||||
<NetworkLogsEntry key={index} entry={currentLog} />
|
||||
{!isSameLogType && <div className="mt-4"/>}
|
||||
</>;
|
||||
})}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ import ScriptErrorIcon from './ScriptErrorIcon';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ResponseSave from 'src/components/ResponsePane/ResponseSave';
|
||||
import ResponseClear from 'src/components/ResponsePane/ResponseClear';
|
||||
import SkippedRequest from './SkippedRequest';
|
||||
import ClearTimeline from './ClearTimeline/index';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
@@ -47,6 +48,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
};
|
||||
|
||||
const response = item.response || {};
|
||||
const responseSize = response.size || 0;
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
@@ -71,7 +73,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
return <Timeline collection={collection} item={item} width={rightPaneWidth} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={item.testResults} assertionResults={item.assertionResults} />;
|
||||
return <TestResults
|
||||
results={item.testResults}
|
||||
assertionResults={item.assertionResults}
|
||||
preRequestTestResults={item.preRequestTestResults}
|
||||
postResponseTestResults={item.postResponseTestResults}
|
||||
/>;
|
||||
}
|
||||
|
||||
default: {
|
||||
@@ -80,6 +87,14 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (item.response && item.status === 'skipped') {
|
||||
return (
|
||||
<StyledWrapper className="flex h-full relative">
|
||||
<SkippedRequest />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading && !item.response) {
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
@@ -112,7 +127,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
};
|
||||
|
||||
const responseHeadersCount = typeof response.headers === 'object' ? Object.entries(response.headers).length : 0;
|
||||
|
||||
|
||||
const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage;
|
||||
|
||||
return (
|
||||
@@ -129,25 +144,30 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
Timeline
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
<TestResultsLabel results={item.testResults} assertionResults={item.assertionResults} />
|
||||
<TestResultsLabel
|
||||
results={item.testResults}
|
||||
assertionResults={item.assertionResults}
|
||||
preRequestTestResults={item.preRequestTestResults}
|
||||
postResponseTestResults={item.postResponseTestResults}
|
||||
/>
|
||||
</div>
|
||||
{!isLoading ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
{hasScriptError && !showScriptErrorCard && (
|
||||
<ScriptErrorIcon
|
||||
itemUid={item.uid}
|
||||
onClick={() => setShowScriptErrorCard(true)}
|
||||
<ScriptErrorIcon
|
||||
itemUid={item.uid}
|
||||
onClick={() => setShowScriptErrorCard(true)}
|
||||
/>
|
||||
)}
|
||||
{focusedTab?.responsePaneTab === "timeline" ? (
|
||||
<ClearTimeline item={item} collection={collection} />
|
||||
) : item?.response ? (
|
||||
) : (item?.response && !item?.response?.error) ? (
|
||||
<>
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<ResponseSave item={item} />
|
||||
<StatusCode status={response.status} />
|
||||
<ResponseTime duration={response.duration} />
|
||||
<ResponseSize size={response.size} />
|
||||
<ResponseSize size={responseSize} />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -158,9 +178,9 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
>
|
||||
{isLoading ? <Overlay item={item} collection={collection} /> : null}
|
||||
{hasScriptError && showScriptErrorCard && (
|
||||
<ScriptError
|
||||
item={item}
|
||||
onClose={() => setShowScriptErrorCard(false)}
|
||||
<ScriptError
|
||||
item={item}
|
||||
onClose={() => setShowScriptErrorCard(false)}
|
||||
/>
|
||||
)}
|
||||
{!item?.response ? (
|
||||
|
||||
@@ -33,6 +33,10 @@ const StyledWrapper = styled.div`
|
||||
.all-tests-passed {
|
||||
color: ${(props) => props.theme.colors.text.green} !important;
|
||||
}
|
||||
|
||||
.skipped-request {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -10,12 +10,13 @@ import ResponseSize from 'components/ResponsePane/ResponseSize';
|
||||
import TestResults from 'components/ResponsePane/TestResults';
|
||||
import TestResultsLabel from 'components/ResponsePane/TestResultsLabel';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import SkippedRequest from 'components/ResponsePane/SkippedRequest';
|
||||
import RunnerTimeline from 'components/ResponsePane/RunnerTimeline';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const [selectedTab, setSelectedTab] = useState('response');
|
||||
|
||||
const { requestSent, responseReceived, testResults, assertionResults, error } = item;
|
||||
const { requestSent, responseReceived, testResults, assertionResults, preRequestTestResults, postResponseTestResults, error } = item;
|
||||
|
||||
const headers = get(item, 'responseReceived.headers', []);
|
||||
const status = get(item, 'responseReceived.status', 0);
|
||||
@@ -48,7 +49,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
return <RunnerTimeline request={requestSent} response={responseReceived} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={testResults} assertionResults={assertionResults} />;
|
||||
return <TestResults
|
||||
results={testResults}
|
||||
assertionResults={assertionResults}
|
||||
preRequestTestResults={preRequestTestResults}
|
||||
postResponseTestResults={postResponseTestResults}
|
||||
/>;
|
||||
}
|
||||
|
||||
default: {
|
||||
@@ -63,6 +69,14 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
});
|
||||
};
|
||||
|
||||
if (item.status === 'skipped') {
|
||||
return (
|
||||
<StyledWrapper className="flex h-full relative">
|
||||
<SkippedRequest />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center px-3 tabs" role="tablist">
|
||||
@@ -77,7 +91,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
Timeline
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
<TestResultsLabel results={testResults} assertionResults={assertionResults} />
|
||||
<TestResultsLabel
|
||||
results={testResults}
|
||||
assertionResults={assertionResults}
|
||||
preRequestTestResults={preRequestTestResults}
|
||||
postResponseTestResults={postResponseTestResults}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<StatusCode status={status} />
|
||||
|
||||
@@ -39,6 +39,10 @@ const Wrapper = styled.div`
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
|
||||
.skipped-request {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { get, cloneDeep } from 'lodash';
|
||||
import { runCollectionFolder, cancelRunnerExecution } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun } from '@tabler/icons';
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
@@ -16,6 +16,28 @@ const getDisplayName = (fullPath, pathname, name = '') => {
|
||||
return path.join(dir, name);
|
||||
};
|
||||
|
||||
const getTestStatus = (results) => {
|
||||
if (!results || !results.length) return 'pass';
|
||||
const failed = results.filter((result) => result.status === 'fail');
|
||||
return failed.length ? 'fail' : 'pass';
|
||||
};
|
||||
|
||||
const allTestsPassed = (item) => {
|
||||
return item.status !== 'error' &&
|
||||
item.testStatus === 'pass' &&
|
||||
item.assertionStatus === 'pass' &&
|
||||
item.preRequestTestStatus === 'pass' &&
|
||||
item.postResponseTestStatus === 'pass';
|
||||
};
|
||||
|
||||
const anyTestFailed = (item) => {
|
||||
return item.status === 'error' ||
|
||||
item.testStatus === 'fail' ||
|
||||
item.assertionStatus === 'fail' ||
|
||||
item.preRequestTestStatus === 'fail' ||
|
||||
item.postResponseTestStatus === 'fail';
|
||||
};
|
||||
|
||||
export default function RunnerResults({ collection }) {
|
||||
const dispatch = useDispatch();
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
@@ -56,19 +78,10 @@ export default function RunnerResults({ collection }) {
|
||||
displayName: getDisplayName(collection.pathname, info.pathname, info.name)
|
||||
};
|
||||
if (newItem.status !== 'error' && newItem.status !== 'skipped') {
|
||||
if (newItem.testResults) {
|
||||
const failed = newItem.testResults.filter((result) => result.status === 'fail');
|
||||
newItem.testStatus = failed.length ? 'fail' : 'pass';
|
||||
} else {
|
||||
newItem.testStatus = 'pass';
|
||||
}
|
||||
|
||||
if (newItem.assertionResults) {
|
||||
const failed = newItem.assertionResults.filter((result) => result.status === 'fail');
|
||||
newItem.assertionStatus = failed.length ? 'fail' : 'pass';
|
||||
} else {
|
||||
newItem.assertionStatus = 'pass';
|
||||
}
|
||||
newItem.testStatus = getTestStatus(newItem.testResults);
|
||||
newItem.assertionStatus = getTestStatus(newItem.assertionResults);
|
||||
newItem.preRequestTestStatus = getTestStatus(newItem.preRequestTestResults);
|
||||
newItem.postResponseTestStatus = getTestStatus(newItem.postResponseTestResults);
|
||||
}
|
||||
return newItem;
|
||||
})
|
||||
@@ -95,13 +108,12 @@ export default function RunnerResults({ collection }) {
|
||||
};
|
||||
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy);
|
||||
const passedRequests = items.filter((item) => {
|
||||
return item.status !== 'error' && item.testStatus === 'pass' && item.assertionStatus === 'pass';
|
||||
});
|
||||
const failedRequests = items.filter((item) => {
|
||||
return (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail';
|
||||
});
|
||||
const passedRequests = items.filter(allTestsPassed);
|
||||
const failedRequests = items.filter(anyTestFailed);
|
||||
|
||||
const skippedRequests = items.filter((item) => {
|
||||
return item.status === 'skipped';
|
||||
});
|
||||
let isCollectionLoading = areItemsLoading(collection);
|
||||
|
||||
if (!items || !items.length) {
|
||||
@@ -159,7 +171,8 @@ export default function RunnerResults({ collection }) {
|
||||
ref={runnerBodyRef}
|
||||
>
|
||||
<div className="pb-2 font-medium test-summary">
|
||||
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}
|
||||
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}, Skipped:{' '}
|
||||
{skippedRequests.length}
|
||||
</div>
|
||||
{runnerInfo?.statusText ?
|
||||
<div className="pb-2 font-medium danger">
|
||||
@@ -172,14 +185,18 @@ export default function RunnerResults({ collection }) {
|
||||
<div className="item-path mt-2">
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{item.status !== 'error' && item.testStatus === 'pass' && item.status !== 'skipped' ? (
|
||||
{allTestsPassed(item) ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
) : (
|
||||
: null}
|
||||
{item.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
:null}
|
||||
{anyTestFailed(item) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
)}
|
||||
:null}
|
||||
</span>
|
||||
<span
|
||||
className={`mr-1 ml-2 ${item.status == 'error' || item.status == 'skipped' || item.testStatus == 'fail' ? 'danger' : ''}`}
|
||||
className={`mr-1 ml-2 ${item.status == 'skipped' ? 'skipped-request' : anyTestFailed(item) ? 'danger' : ''}`}
|
||||
>
|
||||
{item.displayName}
|
||||
</span>
|
||||
@@ -200,6 +217,46 @@ export default function RunnerResults({ collection }) {
|
||||
{item.status == 'error' ? <div className="error-message pl-8 pt-2 text-xs">{item.error}</div> : null}
|
||||
|
||||
<ul className="pl-8">
|
||||
{item.preRequestTestResults
|
||||
? item.preRequestTestResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.description}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.description}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.postResponseTestResults
|
||||
? item.postResponseTestResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.description}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.description}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.testResults
|
||||
? item.testResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
@@ -263,11 +320,15 @@ export default function RunnerResults({ collection }) {
|
||||
<div className="flex items-center px-3 mb-4 font-medium">
|
||||
<span className="mr-2">{selectedItem.displayName}</span>
|
||||
<span>
|
||||
{selectedItem.testStatus === 'pass' ? (
|
||||
{allTestsPassed(selectedItem) ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
) : (
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
)}
|
||||
: null}
|
||||
{anyTestFailed(selectedItem) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{selectedItem.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
</span>
|
||||
</div>
|
||||
<ResponsePane item={selectedItem} collection={collection} />
|
||||
|
||||
@@ -7,8 +7,11 @@ import exportBrunoCollection from 'utils/collections/export';
|
||||
import exportPostmanCollection from 'utils/exporters/postman-collection';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { findCollectionByUid } from 'utils/collections/index';
|
||||
|
||||
const ShareCollection = ({ onClose, collection }) => {
|
||||
const ShareCollection = ({ onClose, collectionUid }) => {
|
||||
const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid));
|
||||
const handleExportBrunoCollection = () => {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
exportBrunoCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -11,11 +11,13 @@ import Help from 'components/Help';
|
||||
import PathDisplay from 'components/PathDisplay';
|
||||
import { useState } from 'react';
|
||||
import { IconArrowBackUp, IconEdit } from "@tabler/icons";
|
||||
import { findCollectionByUid } from 'utils/collections/index';
|
||||
|
||||
const CloneCollection = ({ onClose, collection }) => {
|
||||
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 { name } = collection;
|
||||
|
||||
const formik = useFormik({
|
||||
@@ -46,7 +48,7 @@ const CloneCollection = ({ onClose, collection }) => {
|
||||
values.collectionName,
|
||||
values.collectionFolderName,
|
||||
values.collectionLocation,
|
||||
collection.pathname
|
||||
collection?.pathname
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
|
||||
@@ -15,7 +15,7 @@ import Portal from 'components/Portal';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
const CloneCollectionItem = ({ collectionUid, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isFolder = isItemAFolder(item);
|
||||
const inputRef = useRef();
|
||||
@@ -49,7 +49,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
.test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value))
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(cloneItem(values.name, values.filename, item.uid, collection.uid))
|
||||
dispatch(cloneItem(values.name, values.filename, item.uid, collectionUid))
|
||||
.then(() => {
|
||||
toast.success('Request cloned!');
|
||||
onClose();
|
||||
@@ -172,8 +172,6 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
) : (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
dirName={path.relative(collection?.pathname, path.dirname(item?.pathname))}
|
||||
baseName={formik.values.filename}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.drag-preview {
|
||||
background-color: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useDragLayer } from 'react-dnd';
|
||||
import {
|
||||
IconFile,
|
||||
IconFolder,
|
||||
} from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
function getItemStyles({ x, y }) {
|
||||
if (Number.isNaN(x) || Number.isNaN(y)) return { display: 'none' };
|
||||
const transform = `translate(${x}px, ${y}px)`;
|
||||
|
||||
return {
|
||||
position: 'fixed',
|
||||
pointerEvents: 'none',
|
||||
top: 0,
|
||||
transform,
|
||||
WebkitTransform: transform,
|
||||
zIndex: 100,
|
||||
};
|
||||
}
|
||||
|
||||
export const CollectionItemDragPreview = () => {
|
||||
const {
|
||||
item,
|
||||
isDragging,
|
||||
clientOffset
|
||||
} = useDragLayer((monitor) => ({
|
||||
item: monitor.getItem(),
|
||||
isDragging: monitor.isDragging(),
|
||||
clientOffset: monitor.getClientOffset(),
|
||||
}));
|
||||
if (!isDragging) return null;
|
||||
const { x, y } = clientOffset || {};
|
||||
const shouldShowFolderIcon = !item.type || item.type === 'folder';
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div style={getItemStyles({ x, y })} className='p-2'>
|
||||
<div className='flex items-center gap-2 border border-gray-500/10 rounded-md px-2 py-1 drag-preview'>
|
||||
{shouldShowFolderIcon ? (
|
||||
<IconFolder size={16} />
|
||||
) : (
|
||||
<IconFile size={16} />
|
||||
)}
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
@@ -7,11 +7,11 @@ import { deleteItem } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { recursivelyGetAllItemUids } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DeleteCollectionItem = ({ onClose, item, collection }) => {
|
||||
const DeleteCollectionItem = ({ onClose, item, collectionUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isFolder = isItemAFolder(item);
|
||||
const onConfirm = () => {
|
||||
dispatch(deleteItem(item.uid, collection.uid)).then(() => {
|
||||
dispatch(deleteItem(item.uid, collectionUid)).then(() => {
|
||||
|
||||
if (isFolder) {
|
||||
// close all tabs that belong to the folder
|
||||
|
||||
@@ -62,6 +62,7 @@ const CodeView = ({ language, item }) => {
|
||||
<CodeEditor
|
||||
readOnly
|
||||
collection={collection}
|
||||
item={item}
|
||||
value={snippet}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
|
||||
@@ -4,15 +4,65 @@ import CodeView from './CodeView';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { isValidUrl } from 'utils/url';
|
||||
import { get } from 'lodash';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import { findEnvironmentInCollection, findItemInCollection, findParentItemInCollection } from 'utils/collections';
|
||||
import { interpolateUrl, interpolateUrlPathParams } from 'utils/url/index';
|
||||
import { getLanguages } from 'utils/codegenerator/targets';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
|
||||
const GenerateCodeItem = ({ collection, item, onClose }) => {
|
||||
const getTreePathFromCollectionToItem = (collection, _itemUid) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _itemUid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
// Function to resolve inherited auth
|
||||
const resolveInheritedAuth = (item, collection) => {
|
||||
const request = item.draft?.request || item.request;
|
||||
const authMode = request?.auth?.mode;
|
||||
|
||||
// If auth is not inherit or no auth defined, return the request as is
|
||||
if (!authMode || authMode !== 'inherit') {
|
||||
return {
|
||||
...request
|
||||
};
|
||||
}
|
||||
|
||||
// Get the tree path from collection to item
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item.uid);
|
||||
|
||||
// Default to collection auth
|
||||
const collectionAuth = get(collection, 'root.request.auth', { mode: 'none' });
|
||||
let effectiveAuth = collectionAuth;
|
||||
let source = 'collection';
|
||||
|
||||
// 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') {
|
||||
effectiveAuth = folderAuth;
|
||||
source = 'folder';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...request,
|
||||
auth: effectiveAuth
|
||||
};
|
||||
};
|
||||
|
||||
const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
const languages = getLanguages();
|
||||
|
||||
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
|
||||
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
|
||||
@@ -44,6 +94,9 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
|
||||
get(item, 'draft.request.params') !== undefined ? get(item, 'draft.request.params') : get(item, 'request.params')
|
||||
);
|
||||
|
||||
// Resolve auth inheritance
|
||||
const resolvedRequest = resolveInheritedAuth(item, collection);
|
||||
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(languages[0]);
|
||||
return (
|
||||
<Modal size="lg" title="Generate Code" handleCancel={onClose} hideFooter={true}>
|
||||
@@ -92,16 +145,10 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
|
||||
language={selectedLanguage}
|
||||
item={{
|
||||
...item,
|
||||
request:
|
||||
item.request.url !== ''
|
||||
? {
|
||||
...item.request,
|
||||
url: finalUrl
|
||||
}
|
||||
: {
|
||||
...item.draft.request,
|
||||
url: finalUrl
|
||||
}
|
||||
request: {
|
||||
...resolvedRequest,
|
||||
url: finalUrl
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -16,7 +16,7 @@ import Portal from 'components/Portal';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
const RenameCollectionItem = ({ collectionUid, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isFolder = isItemAFolder(item);
|
||||
const inputRef = useRef();
|
||||
@@ -57,13 +57,13 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
return;
|
||||
}
|
||||
if (!isFolder && item.draft) {
|
||||
await dispatch(saveRequest(item.uid, collection.uid, true));
|
||||
await dispatch(saveRequest(item.uid, collectionUid, true));
|
||||
}
|
||||
const { name: newName, filename: newFilename } = values;
|
||||
try {
|
||||
let renameConfig = {
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
collectionUid,
|
||||
};
|
||||
renameConfig['newName'] = newName;
|
||||
if (itemFilename !== newFilename) {
|
||||
@@ -191,8 +191,6 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
) : (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
dirName={path.relative(collection?.pathname, path.dirname(item?.pathname))}
|
||||
baseName={formik.values.filename}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,16 +2,19 @@ import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { uuid } from 'utils/common';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { flattenItems } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
|
||||
const RunCollectionItem = ({ collection, item, onClose }) => {
|
||||
const RunCollectionItem = ({ collectionUid, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
|
||||
const isCollectionRunInProgress = collection?.runnerResult?.info?.status && (collection?.runnerResult?.info?.status !== 'ended');
|
||||
|
||||
const onSubmit = (recursive) => {
|
||||
dispatch(
|
||||
addTab({
|
||||
@@ -20,10 +23,24 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
|
||||
type: 'collection-runner'
|
||||
})
|
||||
);
|
||||
dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive));
|
||||
if (!isCollectionRunInProgress) {
|
||||
dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive));
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleViewRunner = (e) => {
|
||||
e.preventDefault();
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'collection-runner'
|
||||
})
|
||||
);
|
||||
onClose();
|
||||
}
|
||||
|
||||
const getRequestsCount = (items) => {
|
||||
const requestTypes = ['http-request', 'graphql-request']
|
||||
return items.filter(req => requestTypes.includes(req.type)).length;
|
||||
@@ -34,8 +51,6 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
|
||||
const recursiveRunLength = getRequestsCount(flattenedItems);
|
||||
|
||||
const isFolderLoading = areItemsLoading(item);
|
||||
console.log(item);
|
||||
console.log(isFolderLoading);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
@@ -55,22 +70,34 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
|
||||
</div>
|
||||
<div className={isFolderLoading ? "mb-2" : "mb-8"}>This will run all the requests in this folder and all its subfolders.</div>
|
||||
{isFolderLoading ? <div className='mb-8 warning'>Requests in this folder are still loading.</div> : null}
|
||||
{isCollectionRunInProgress ? <div className='mb-6 warning'>A Collection Run is already in progress.</div> : null}
|
||||
<div className="flex justify-end bruno-modal-footer">
|
||||
<span className="mr-3">
|
||||
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" disabled={!recursiveRunLength} className="submit btn btn-md btn-secondary mr-3" onClick={() => onSubmit(true)}>
|
||||
Recursive Run
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" disabled={!runLength} className="submit btn btn-md btn-secondary" onClick={() => onSubmit(false)}>
|
||||
Run
|
||||
</button>
|
||||
</span>
|
||||
{
|
||||
isCollectionRunInProgress ?
|
||||
<span>
|
||||
<button type="submit" className="submit btn btn-md btn-secondary mr-3" onClick={handleViewRunner}>
|
||||
View Run
|
||||
</button>
|
||||
</span>
|
||||
:
|
||||
<>
|
||||
<span>
|
||||
<button type="submit" disabled={!recursiveRunLength} className="submit btn btn-md btn-secondary mr-3" onClick={() => onSubmit(true)}>
|
||||
Recursive Run
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" disabled={!runLength} className="submit btn btn-md btn-secondary" onClick={() => onSubmit(false)}>
|
||||
Run
|
||||
</button>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
.menu-icon {
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
|
||||
@@ -22,6 +23,65 @@ const Wrapper = styled.div`
|
||||
height: 1.875rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
|
||||
/* Common styles for drop indicators */
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: ${(props) => props.theme.dragAndDrop.border};
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
/* Drop target styles */
|
||||
&.drop-target {
|
||||
background-color: ${(props) => props.theme.dragAndDrop.hoverBg};
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.drop-target-above {
|
||||
&::before {
|
||||
opacity: 1;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.drop-target-below {
|
||||
&::after {
|
||||
opacity: 1;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Inside drop target style */
|
||||
&.drop-target {
|
||||
&::before {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
opacity: 1;
|
||||
background: ${(props) => props.theme.dragAndDrop.hoverBg};
|
||||
border: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
|
||||
// border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.rotate-90 {
|
||||
transform: rotateZ(90deg);
|
||||
@@ -45,6 +105,20 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
&.item-target {
|
||||
background: #ccc3;
|
||||
}
|
||||
|
||||
&.item-seperator {
|
||||
.seperator {
|
||||
bottom: 0px;
|
||||
position: absolute;
|
||||
height: 3px;
|
||||
width: 100%;
|
||||
background: #ccc3;
|
||||
}
|
||||
}
|
||||
|
||||
&.item-focused-in-tab {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.bg};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useRef, forwardRef, useEffect } from 'react';
|
||||
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import range from 'lodash/range';
|
||||
import filter from 'lodash/filter';
|
||||
import classnames from 'classnames';
|
||||
@@ -6,7 +7,7 @@ import { useDrag, useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { addTab, focusTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
|
||||
import { moveItem, showInFolder, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { handleCollectionItemDrop, sendRequest, showInFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
@@ -16,7 +17,7 @@ import CloneCollectionItem from './CloneCollectionItem';
|
||||
import DeleteCollectionItem from './DeleteCollectionItem';
|
||||
import RunCollectionItem from './RunCollectionItem';
|
||||
import GenerateCodeItem from './GenerateCodeItem';
|
||||
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
|
||||
import { isItemARequest, isItemAFolder } from 'utils/tabs';
|
||||
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
@@ -26,13 +27,22 @@ import NetworkError from 'components/ResponsePane/NetworkError/index';
|
||||
import CollectionItemInfo from './CollectionItemInfo/index';
|
||||
import CollectionItemIcon from './CollectionItemIcon';
|
||||
import { scrollToTheActiveTab } from 'utils/tabs';
|
||||
import { isTabForItemActive as isTabForItemActiveSelector, isTabForItemPresent as isTabForItemPresentSelector } from 'src/selectors/tab';
|
||||
import { isEqual } from 'lodash';
|
||||
import { calculateDraggedItemNewPathname } from 'utils/collections/index';
|
||||
|
||||
const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) => {
|
||||
const _isTabForItemActiveSelector = isTabForItemActiveSelector({ itemUid: item.uid });
|
||||
const isTabForItemActive = useSelector(_isTabForItemActiveSelector, isEqual);
|
||||
|
||||
const _isTabForItemPresentSelector = isTabForItemPresentSelector({ itemUid: item.uid });
|
||||
const isTabForItemPresent = useSelector(_isTabForItemPresentSelector, isEqual);
|
||||
|
||||
const isSidebarDragging = useSelector((state) => state.app.isDragging);
|
||||
const dispatch = useDispatch();
|
||||
const collectionItemRef = useRef(null);
|
||||
|
||||
// We use a single ref for drag and drop.
|
||||
const ref = useRef(null);
|
||||
|
||||
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
|
||||
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
|
||||
@@ -44,10 +54,13 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const [itemInfoModalOpen, setItemInfoModalOpen] = useState(false);
|
||||
const hasSearchText = searchText && searchText?.trim()?.length;
|
||||
const itemIsCollapsed = hasSearchText ? false : item.collapsed;
|
||||
const isFolder = isItemAFolder(item);
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: `collection-item-${collection.uid}`,
|
||||
item: item,
|
||||
const [dropType, setDropType] = useState(null); // 'adjacent' or 'inside'
|
||||
|
||||
const [{ isDragging }, drag, dragPreview] = useDrag({
|
||||
type: `collection-item-${collectionUid}`,
|
||||
item,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging()
|
||||
}),
|
||||
@@ -56,21 +69,72 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
}
|
||||
});
|
||||
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
accept: `collection-item-${collection.uid}`,
|
||||
drop: (draggedItem) => {
|
||||
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
|
||||
useEffect(() => {
|
||||
dragPreview(getEmptyImage(), { captureDraggingState: true });
|
||||
}, []);
|
||||
|
||||
const determineDropType = (monitor) => {
|
||||
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
if (!hoverBoundingRect || !clientOffset) return null;
|
||||
|
||||
const clientY = clientOffset.y - hoverBoundingRect.top;
|
||||
const folderUpperThreshold = hoverBoundingRect.height * 0.35;
|
||||
const fileUpperThreshold = hoverBoundingRect.height * 0.5;
|
||||
|
||||
if (isItemAFolder(item)) {
|
||||
return clientY < folderUpperThreshold ? 'adjacent' : 'inside';
|
||||
} else {
|
||||
return clientY < fileUpperThreshold ? 'adjacent' : null;
|
||||
}
|
||||
};
|
||||
|
||||
const canItemBeDropped = ({ draggedItem, targetItem, dropType }) => {
|
||||
const { uid: targetItemUid, pathname: targetItemPathname } = targetItem;
|
||||
const { uid: draggedItemUid, pathname: draggedItemPathname } = draggedItem;
|
||||
|
||||
if (draggedItemUid === targetItemUid) return false;
|
||||
|
||||
const newPathname = calculateDraggedItemNewPathname({ draggedItem, targetItem, dropType, collectionPathname });
|
||||
if (!newPathname) return false;
|
||||
|
||||
if (targetItemPathname?.startsWith(draggedItemPathname)) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const [{ isOver, canDrop }, drop] = useDrop({
|
||||
accept: `collection-item-${collectionUid}`,
|
||||
hover: (draggedItem, monitor) => {
|
||||
const { uid: targetItemUid } = item;
|
||||
const { uid: draggedItemUid } = draggedItem;
|
||||
|
||||
if (draggedItemUid === targetItemUid) return;
|
||||
|
||||
const dropType = determineDropType(monitor);
|
||||
|
||||
const _canItemBeDropped = canItemBeDropped({ draggedItem, targetItem: item, dropType });
|
||||
|
||||
setDropType(_canItemBeDropped ? dropType : null);
|
||||
},
|
||||
canDrop: (draggedItem) => {
|
||||
return draggedItem.uid !== item.uid;
|
||||
drop: async (draggedItem, monitor) => {
|
||||
const { uid: targetItemUid } = item;
|
||||
const { uid: draggedItemUid } = draggedItem;
|
||||
|
||||
if (draggedItemUid === targetItemUid) return;
|
||||
|
||||
const dropType = determineDropType(monitor);
|
||||
if (!dropType) return;
|
||||
|
||||
await dispatch(handleCollectionItemDrop({ targetItem: item, draggedItem, dropType, collectionUid }))
|
||||
setDropType(null);
|
||||
},
|
||||
canDrop: (draggedItem) => draggedItem.uid !== item.uid,
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
isOver: monitor.isOver()
|
||||
}),
|
||||
});
|
||||
|
||||
drag(drop(collectionItemRef));
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const MenuIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
@@ -84,13 +148,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
'rotate-90': !itemIsCollapsed
|
||||
});
|
||||
|
||||
const itemRowClassName = classnames('flex collection-item-name items-center', {
|
||||
'item-focused-in-tab': item.uid == activeTabUid,
|
||||
'item-hovered': isOver
|
||||
const itemRowClassName = classnames('flex collection-item-name relative items-center', {
|
||||
'item-focused-in-tab': isTabForItemActive,
|
||||
'item-hovered': isOver && canDrop,
|
||||
'drop-target': isOver && dropType === 'inside',
|
||||
'drop-target-above': isOver && dropType === 'adjacent'
|
||||
});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendRequest(item, collection.uid)).catch((err) =>
|
||||
dispatch(sendRequest(item, collectionUid)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
duration: 5000
|
||||
})
|
||||
@@ -101,12 +167,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
if (event && event.detail != 1) return;
|
||||
//scroll to the active tab
|
||||
setTimeout(scrollToTheActiveTab, 50);
|
||||
|
||||
const isRequest = isItemARequest(item);
|
||||
|
||||
if (isRequest) {
|
||||
dispatch(hideHomePage());
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
if (isTabForItemPresent) {
|
||||
dispatch(
|
||||
focusTab({
|
||||
uid: item.uid
|
||||
@@ -114,11 +178,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
collectionUid: collectionUid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item),
|
||||
type: 'request',
|
||||
})
|
||||
@@ -127,14 +190,14 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
collectionUid: collectionUid,
|
||||
type: 'folder-settings',
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
collectionFolderClicked({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
collectionUid: collectionUid
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -146,10 +209,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
dispatch(
|
||||
collectionFolderClicked({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
collectionUid: collectionUid
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRightClick = (event) => {
|
||||
const _menuDropdown = dropdownTippyRef.current;
|
||||
@@ -164,7 +227,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
|
||||
let indents = range(item.depth);
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const isFolder = isItemAFolder(item);
|
||||
|
||||
const className = classnames('flex flex-col w-full', {
|
||||
'is-sidebar-dragging': isSidebarDragging
|
||||
@@ -183,49 +245,14 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
}
|
||||
|
||||
const handleDoubleClick = (event) => {
|
||||
dispatch(makeTabPermanent({ uid: item.uid }))
|
||||
dispatch(makeTabPermanent({ uid: item.uid }));
|
||||
};
|
||||
|
||||
// we need to sort request items by seq property
|
||||
const sortRequestItems = (items = []) => {
|
||||
// Sort items by their "seq" property.
|
||||
const sortItemsBySequence = (items = []) => {
|
||||
return items.sort((a, b) => a.seq - b.seq);
|
||||
};
|
||||
|
||||
// we need to sort folder items by name alphabetically
|
||||
const sortFolderItems = (items = []) => {
|
||||
return items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
const handleGenerateCode = (e) => {
|
||||
e.stopPropagation();
|
||||
dropdownTippyRef.current.hide();
|
||||
if (item?.request?.url !== '' || (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')) {
|
||||
setGenerateCodeItemModalOpen(true);
|
||||
} else {
|
||||
toast.error('URL is required');
|
||||
}
|
||||
};
|
||||
|
||||
const viewFolderSettings = () => {
|
||||
if (isItemAFolder(item)) {
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
dispatch(
|
||||
focusTab({
|
||||
uid: item.uid
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
type: 'folder-settings'
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowInFolder = () => {
|
||||
dispatch(showInFolder(item.pathname)).catch((error) => {
|
||||
console.error('Error opening the folder', error);
|
||||
@@ -233,62 +260,89 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
|
||||
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
|
||||
const folderItems = sortItemsBySequence(filter(item.items, (i) => isItemAFolder(i)));
|
||||
const requestItems = sortItemsBySequence(filter(item.items, (i) => isItemARequest(i)));
|
||||
|
||||
const handleGenerateCode = (e) => {
|
||||
e.stopPropagation();
|
||||
dropdownTippyRef.current.hide();
|
||||
if (
|
||||
(item?.request?.url !== '') ||
|
||||
(item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')
|
||||
) {
|
||||
setGenerateCodeItemModalOpen(true);
|
||||
} else {
|
||||
toast.error('URL is required');
|
||||
}
|
||||
};
|
||||
|
||||
const viewFolderSettings = () => {
|
||||
if (isItemAFolder(item)) {
|
||||
if (isTabForItemPresent) {
|
||||
dispatch(focusTab({ uid: item.uid }));
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid,
|
||||
type: 'folder-settings'
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className={className}>
|
||||
{renameItemModalOpen && (
|
||||
<RenameCollectionItem item={item} collection={collection} onClose={() => setRenameItemModalOpen(false)} />
|
||||
<RenameCollectionItem item={item} collectionUid={collectionUid} onClose={() => setRenameItemModalOpen(false)} />
|
||||
)}
|
||||
{cloneItemModalOpen && (
|
||||
<CloneCollectionItem item={item} collection={collection} onClose={() => setCloneItemModalOpen(false)} />
|
||||
<CloneCollectionItem item={item} collectionUid={collectionUid} onClose={() => setCloneItemModalOpen(false)} />
|
||||
)}
|
||||
{deleteItemModalOpen && (
|
||||
<DeleteCollectionItem item={item} collection={collection} onClose={() => setDeleteItemModalOpen(false)} />
|
||||
<DeleteCollectionItem item={item} collectionUid={collectionUid} onClose={() => setDeleteItemModalOpen(false)} />
|
||||
)}
|
||||
{newRequestModalOpen && (
|
||||
<NewRequest item={item} collection={collection} onClose={() => setNewRequestModalOpen(false)} />
|
||||
<NewRequest item={item} collectionUid={collectionUid} onClose={() => setNewRequestModalOpen(false)} />
|
||||
)}
|
||||
{newFolderModalOpen && (
|
||||
<NewFolder item={item} collection={collection} onClose={() => setNewFolderModalOpen(false)} />
|
||||
<NewFolder item={item} collectionUid={collectionUid} onClose={() => setNewFolderModalOpen(false)} />
|
||||
)}
|
||||
{runCollectionModalOpen && (
|
||||
<RunCollectionItem collection={collection} item={item} onClose={() => setRunCollectionModalOpen(false)} />
|
||||
<RunCollectionItem collectionUid={collectionUid} item={item} onClose={() => setRunCollectionModalOpen(false)} />
|
||||
)}
|
||||
{generateCodeItemModalOpen && (
|
||||
<GenerateCodeItem collection={collection} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
|
||||
<GenerateCodeItem collectionUid={collectionUid} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
|
||||
)}
|
||||
{itemInfoModalOpen && (
|
||||
<CollectionItemInfo item={item} collection={collection} onClose={() => setItemInfoModalOpen(false)} />
|
||||
<CollectionItemInfo item={item} onClose={() => setItemInfoModalOpen(false)} />
|
||||
)}
|
||||
<div className={itemRowClassName} ref={collectionItemRef}>
|
||||
<div
|
||||
className={itemRowClassName}
|
||||
ref={(node) => {
|
||||
ref.current = node;
|
||||
drag(drop(node));
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center h-full w-full">
|
||||
{indents && indents.length
|
||||
? indents.map((i) => {
|
||||
return (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
className="indent-block"
|
||||
key={i}
|
||||
style={{
|
||||
width: 16,
|
||||
minWidth: 16,
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
{/* Indent */}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
? indents.map((i) => (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
className="indent-block"
|
||||
key={i}
|
||||
style={{ width: 16, minWidth: 16, height: '100%' }}
|
||||
>
|
||||
{/* Indent */}
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
<div
|
||||
className="flex flex-grow items-center h-full overflow-hidden"
|
||||
style={{
|
||||
paddingLeft: 8
|
||||
}}
|
||||
style={{ paddingLeft: 8 }}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
@@ -304,10 +358,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="ml-1 flex w-full h-full items-center overflow-hidden"
|
||||
>
|
||||
<div className="ml-1 flex w-full h-full items-center overflow-hidden">
|
||||
<CollectionItemIcon item={item} />
|
||||
<span className="item-name" title={item.name}>
|
||||
{item.name}
|
||||
@@ -429,17 +480,16 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!itemIsCollapsed ? (
|
||||
<div>
|
||||
{folderItems && folderItems.length
|
||||
? folderItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
return <CollectionItem key={i.uid} item={i} collectionUid={collectionUid} collectionPathname={collectionPathname} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
{requestItems && requestItems.length
|
||||
? requestItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
return <CollectionItem key={i.uid} item={i} collectionUid={collectionUid} collectionPathname={collectionPathname} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
@@ -448,4 +498,4 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionItem;
|
||||
export default React.memo(CollectionItem);
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import exportBrunoCollection from 'utils/collections/export';
|
||||
import exportPostmanCollection from 'utils/exporters/postman-collection';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import Modal from 'components/Modal';
|
||||
import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index';
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { IconFiles } from '@tabler/icons';
|
||||
import { removeCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findCollectionByUid } from 'utils/collections/index';
|
||||
|
||||
const RemoveCollection = ({ onClose, collection }) => {
|
||||
const RemoveCollection = ({ onClose, collectionUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid));
|
||||
|
||||
const onConfirm = () => {
|
||||
dispatch(removeCollection(collection.uid))
|
||||
|
||||
@@ -2,13 +2,15 @@ import React, { useRef, useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
import { renameCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findCollectionByUid } from 'utils/collections/index';
|
||||
|
||||
const RenameCollection = ({ collection, onClose }) => {
|
||||
const RenameCollection = ({ collectionUid, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid));
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
|
||||
@@ -13,7 +13,8 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
&.item-hovered {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
border-top: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
|
||||
border-bottom: 2px solid transparent;
|
||||
.collection-actions {
|
||||
.dropdown {
|
||||
div[aria-expanded='false'] {
|
||||
@@ -62,6 +63,36 @@ const Wrapper = styled.div`
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&.drop-target {
|
||||
background-color: ${(props) => props.theme.dragAndDrop.hoverBg};
|
||||
transition: ${(props) => props.theme.dragAndDrop.transition};
|
||||
}
|
||||
|
||||
&.drop-target-above {
|
||||
border: none;
|
||||
border-top: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
|
||||
margin-top: -2px;
|
||||
background: transparent;
|
||||
transition: ${(props) => props.theme.dragAndDrop.transition};
|
||||
}
|
||||
|
||||
&.drop-target-below {
|
||||
border: none;
|
||||
border-bottom: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
|
||||
margin-bottom: -2px;
|
||||
background: transparent;
|
||||
transition: ${(props) => props.theme.dragAndDrop.transition};
|
||||
}
|
||||
}
|
||||
|
||||
.collection-name.drop-target {
|
||||
border: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
|
||||
border-radius: 4px;
|
||||
background-color: ${(props) => props.theme.dragAndDrop.hoverBg};
|
||||
margin: -2px;
|
||||
transition: ${(props) => props.theme.dragAndDrop.transition};
|
||||
box-shadow: 0 0 0 2px ${(props) => props.theme.dragAndDrop.hoverBg};
|
||||
}
|
||||
|
||||
#sidebar-collection-name {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, forwardRef, useRef, useEffect } from 'react';
|
||||
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import classnames from 'classnames';
|
||||
import { uuid } from 'utils/common';
|
||||
import filter from 'lodash/filter';
|
||||
@@ -6,8 +7,8 @@ import { useDrop, useDrag } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { collapseCollection } from 'providers/ReduxStore/slices/collections';
|
||||
import { mountCollection, moveItemToRootOfCollection, moveCollectionAndPersist } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { mountCollection, moveCollectionAndPersist, handleCollectionItemDrop } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
@@ -19,9 +20,10 @@ import { isItemAFolder, isItemARequest } from 'utils/collections';
|
||||
import RenameCollection from './RenameCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import CloneCollection from './CloneCollection';
|
||||
import { areItemsLoading, findItemInCollection } from 'utils/collections';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
import { scrollToTheActiveTab } from 'utils/tabs';
|
||||
import ShareCollection from 'components/ShareCollection/index';
|
||||
import { CollectionItemDragPreview } from './CollectionItem/CollectionItemDragPreview/index';
|
||||
|
||||
const Collection = ({ collection, searchText }) => {
|
||||
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
|
||||
@@ -33,7 +35,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isLoading = areItemsLoading(collection);
|
||||
const collectionRef = useRef(null);
|
||||
|
||||
|
||||
const menuDropdownTippyRef = useRef();
|
||||
const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref);
|
||||
const MenuIcon = forwardRef((props, ref) => {
|
||||
@@ -127,8 +129,8 @@ const Collection = ({ collection, searchText }) => {
|
||||
const isCollectionItem = (itemType) => {
|
||||
return itemType.startsWith('collection-item');
|
||||
};
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
|
||||
const [{ isDragging }, drag, dragPreview] = useDrag({
|
||||
type: "collection",
|
||||
item: collection,
|
||||
collect: (monitor) => ({
|
||||
@@ -144,7 +146,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
drop: (draggedItem, monitor) => {
|
||||
const itemType = monitor.getItemType();
|
||||
if (isCollectionItem(itemType)) {
|
||||
dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid))
|
||||
dispatch(handleCollectionItemDrop({ targetItem: collection, draggedItem, dropType: 'inside', collectionUid: collection.uid }))
|
||||
} else {
|
||||
dispatch(moveCollectionAndPersist({draggedItem, targetItem: collection}));
|
||||
}
|
||||
@@ -157,7 +159,9 @@ const Collection = ({ collection, searchText }) => {
|
||||
}),
|
||||
});
|
||||
|
||||
drag(drop(collectionRef));
|
||||
useEffect(() => {
|
||||
dragPreview(getEmptyImage(), { captureDraggingState: true });
|
||||
}, []);
|
||||
|
||||
if (searchText && searchText.length) {
|
||||
if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) {
|
||||
@@ -170,36 +174,35 @@ const Collection = ({ collection, searchText }) => {
|
||||
});
|
||||
|
||||
// we need to sort request items by seq property
|
||||
const sortRequestItems = (items = []) => {
|
||||
const sortItemsBySequence = (items = []) => {
|
||||
return items.sort((a, b) => a.seq - b.seq);
|
||||
};
|
||||
|
||||
// we need to sort folder items by name alphabetically
|
||||
const sortFolderItems = (items = []) => {
|
||||
return items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
const requestItems = sortRequestItems(filter(collection.items, (i) => isItemARequest(i)));
|
||||
const folderItems = sortFolderItems(filter(collection.items, (i) => isItemAFolder(i)));
|
||||
const requestItems = sortItemsBySequence(filter(collection.items, (i) => isItemARequest(i)));
|
||||
const folderItems = sortItemsBySequence(filter(collection.items, (i) => isItemAFolder(i)));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col">
|
||||
{showNewRequestModal && <NewRequest collection={collection} onClose={() => setShowNewRequestModal(false)} />}
|
||||
{showNewFolderModal && <NewFolder collection={collection} onClose={() => setShowNewFolderModal(false)} />}
|
||||
{showNewRequestModal && <NewRequest collectionUid={collection.uid} onClose={() => setShowNewRequestModal(false)} />}
|
||||
{showNewFolderModal && <NewFolder collectionUid={collection.uid} onClose={() => setShowNewFolderModal(false)} />}
|
||||
{showRenameCollectionModal && (
|
||||
<RenameCollection collection={collection} onClose={() => setShowRenameCollectionModal(false)} />
|
||||
<RenameCollection collectionUid={collection.uid} onClose={() => setShowRenameCollectionModal(false)} />
|
||||
)}
|
||||
{showRemoveCollectionModal && (
|
||||
<RemoveCollection collection={collection} onClose={() => setShowRemoveCollectionModal(false)} />
|
||||
<RemoveCollection collectionUid={collection.uid} onClose={() => setShowRemoveCollectionModal(false)} />
|
||||
)}
|
||||
{showShareCollectionModal && (
|
||||
<ShareCollection collection={collection} onClose={() => setShowShareCollectionModal(false)} />
|
||||
<ShareCollection collectionUid={collection.uid} onClose={() => setShowShareCollectionModal(false)} />
|
||||
)}
|
||||
{showCloneCollectionModalOpen && (
|
||||
<CloneCollection collection={collection} onClose={() => setShowCloneCollectionModalOpen(false)} />
|
||||
<CloneCollection collectionUid={collection.uid} onClose={() => setShowCloneCollectionModalOpen(false)} />
|
||||
)}
|
||||
<CollectionItemDragPreview />
|
||||
<div className={collectionRowClassName}
|
||||
ref={collectionRef}
|
||||
ref={(node) => {
|
||||
collectionRef.current = node;
|
||||
drag(drop(node));
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex flex-grow items-center overflow-hidden"
|
||||
@@ -296,20 +299,15 @@ const Collection = ({ collection, searchText }) => {
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{!collectionIsCollapsed ? (
|
||||
<div>
|
||||
{folderItems && folderItems.length
|
||||
? folderItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
{requestItems && requestItems.length
|
||||
? requestItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
{folderItems?.map?.((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collectionUid={collection.uid} collectionPathname={collection.pathname} searchText={searchText} />;
|
||||
})}
|
||||
{requestItems?.map?.((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collectionUid={collection.uid} collectionPathname={collection.pathname} searchText={searchText} />;
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,8 @@ import PathDisplay from 'components/PathDisplay/index';
|
||||
import { useState } from 'react';
|
||||
import { IconArrowBackUp, IconEdit } from '@tabler/icons';
|
||||
import Help from 'components/Help';
|
||||
import { multiLineMsg } from "utils/common";
|
||||
import { formatIpcError } from "utils/common/error";
|
||||
|
||||
const CreateCollection = ({ onClose }) => {
|
||||
const inputRef = useRef();
|
||||
@@ -45,7 +47,7 @@ const CreateCollection = ({ onClose }) => {
|
||||
toast.success('Collection created!');
|
||||
onClose();
|
||||
})
|
||||
.catch((e) => toast.error('An error occurred while creating the collection - ' + e));
|
||||
.catch((e) => toast.error(multiLineMsg('An error occurred while creating the collection', formatIpcError(e))));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -113,7 +115,6 @@ const CreateCollection = ({ onClose }) => {
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full cursor-pointer"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
@@ -121,6 +122,9 @@ const CreateCollection = ({ onClose }) => {
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
onChange={e => {
|
||||
formik.setFieldValue('collectionLocation', e.target.value);
|
||||
}}
|
||||
/>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
|
||||
@@ -1,42 +1,43 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { IconLoader2 } from '@tabler/icons';
|
||||
import importBrunoCollection from 'utils/importers/bruno-collection';
|
||||
import importPostmanCollection from 'utils/importers/postman-collection';
|
||||
import { postmanToBruno, readFile } from 'utils/importers/postman-collection';
|
||||
import importInsomniaCollection from 'utils/importers/insomnia-collection';
|
||||
import importOpenapiCollection from 'utils/importers/openapi-collection';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
import fileDialog from 'file-dialog';
|
||||
|
||||
const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
const [options, setOptions] = useState({
|
||||
enablePostmanTranslations: {
|
||||
enabled: true,
|
||||
label: 'Auto translate postman scripts',
|
||||
subLabel:
|
||||
"When enabled, Bruno will try as best to translate the scripts from the imported collection to Bruno's format."
|
||||
}
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleImportBrunoCollection = () => {
|
||||
importBrunoCollection()
|
||||
.then(({ collection }) => {
|
||||
handleSubmit({ collection });
|
||||
})
|
||||
.catch((err) => toastError(err, 'Import collection failed'));
|
||||
.catch((err) => toastError(err, 'Import collection failed'))
|
||||
};
|
||||
|
||||
|
||||
const handleImportPostmanCollection = () => {
|
||||
importPostmanCollection(options)
|
||||
.then(({ collection, translationLog }) => {
|
||||
handleSubmit({ collection, translationLog });
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then((...args) => {
|
||||
setIsLoading(true);
|
||||
return readFile(...args);
|
||||
})
|
||||
.catch((err) => toastError(err, 'Postman Import collection failed'));
|
||||
};
|
||||
.then((collection) => postmanToBruno(collection))
|
||||
.then((collection) => handleSubmit({ collection }))
|
||||
.catch((err) => toastError(err, 'Postman Import collection failed'))
|
||||
.finally(() => setIsLoading(false));
|
||||
}
|
||||
|
||||
const handleImportInsomniaCollection = () => {
|
||||
importInsomniaCollection()
|
||||
.then(({ collection }) => {
|
||||
handleSubmit({ collection });
|
||||
})
|
||||
.catch((err) => toastError(err, 'Insomnia Import collection failed'));
|
||||
.catch((err) => toastError(err, 'Insomnia Import collection failed'))
|
||||
};
|
||||
|
||||
const handleImportOpenapiCollection = () => {
|
||||
@@ -44,17 +45,9 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
.then(({ collection }) => {
|
||||
handleSubmit({ collection });
|
||||
})
|
||||
.catch((err) => toastError(err, 'OpenAPI v3 Import collection failed'));
|
||||
};
|
||||
const toggleOptions = (event, optionKey) => {
|
||||
setOptions({
|
||||
...options,
|
||||
[optionKey]: {
|
||||
...options[optionKey],
|
||||
enabled: !options[optionKey].enabled
|
||||
}
|
||||
});
|
||||
.catch((err) => toastError(err, 'OpenAPI v3 Import collection failed'))
|
||||
};
|
||||
|
||||
const CollectionButton = ({ children, className, onClick }) => {
|
||||
return (
|
||||
<button
|
||||
@@ -67,43 +60,67 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
</button>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleCancel={onClose}>
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-sm">Select the type of your existing collection :</h3>
|
||||
<div className="mt-4 grid grid-rows-2 grid-flow-col gap-2">
|
||||
<CollectionButton onClick={handleImportBrunoCollection}>Bruno Collection</CollectionButton>
|
||||
<CollectionButton onClick={handleImportPostmanCollection}>Postman Collection</CollectionButton>
|
||||
<CollectionButton onClick={handleImportInsomniaCollection}>Insomnia Collection</CollectionButton>
|
||||
<CollectionButton onClick={handleImportOpenapiCollection}>OpenAPI V3 Spec</CollectionButton>
|
||||
</div>
|
||||
<div className="flex justify-start w-full mt-4 max-w-[450px]">
|
||||
{Object.entries(options || {}).map(([key, option]) => (
|
||||
<div key={key} className="relative flex items-start">
|
||||
<div className="flex h-6 items-center">
|
||||
<input
|
||||
id="comments"
|
||||
aria-describedby="comments-description"
|
||||
name="comments"
|
||||
type="checkbox"
|
||||
checked={option.enabled}
|
||||
onChange={(e) => toggleOptions(e, key)}
|
||||
className="h-3.5 w-3.5 rounded border-zinc-300 dark:ring-offset-zinc-800 bg-transparent text-indigo-600 dark:text-indigo-500 focus:ring-indigo-600 dark:focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-2 text-sm leading-6">
|
||||
<label htmlFor="comments" className="font-medium text-gray-900 dark:text-zinc-50">
|
||||
{option.label}
|
||||
</label>
|
||||
<p id="comments-description" className="text-zinc-500 text-xs dark:text-zinc-400">
|
||||
{option.subLabel}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
const FullscreenLoader = () => {
|
||||
const [loadingMessage, setLoadingMessage] = useState('');
|
||||
|
||||
// Messages to cycle through while loading
|
||||
const loadingMessages = [
|
||||
'Processing collection...',
|
||||
'Analyzing requests...',
|
||||
'Translating scripts...',
|
||||
'Preparing collection...',
|
||||
'Almost done...'
|
||||
];
|
||||
|
||||
|
||||
// Cycle through loading messages for better UX
|
||||
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>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <FullscreenLoader />}
|
||||
{!isLoading && (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleCancel={onClose}>
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-sm">Select the type of your existing collection :</h3>
|
||||
<div className="mt-4 grid grid-rows-2 grid-flow-col gap-2">
|
||||
<CollectionButton onClick={handleImportBrunoCollection}>Bruno Collection</CollectionButton>
|
||||
<CollectionButton onClick={handleImportPostmanCollection}>Postman Collection</CollectionButton>
|
||||
<CollectionButton onClick={handleImportInsomniaCollection}>Insomnia Collection</CollectionButton>
|
||||
<CollectionButton onClick={handleImportOpenapiCollection}>OpenAPI V3 Spec</CollectionButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,105 +4,8 @@ import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Modal from 'components/Modal';
|
||||
import { IconAlertTriangle, IconArrowRight, IconCaretDown, IconCaretRight, IconCopy } from '@tabler/icons';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const TranslationLog = ({ translationLog }) => {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const preventSetShowDetails = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowDetails(!showDetails);
|
||||
};
|
||||
const copyClipboard = (e, value) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
navigator.clipboard.writeText(value);
|
||||
toast.success('Copied to clipboard');
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col mt-2">
|
||||
<div className="border-l-2 border-amber-500 dark:border-amber-300 bg-amber-50 dark:bg-amber-50/10 p-1.5 rounded-r">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<IconAlertTriangle className="h-4 w-4 text-amber-500 dark:text-amber-300" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ml-2">
|
||||
<p className="text-xs text-amber-700 dark:text-amber-300">
|
||||
<span className="font-semibold">Warning:</span> Some commands were not translated.{' '}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => preventSetShowDetails(e)}
|
||||
className="flex w-fit items-center rounded px-2.5 py-1 mt-2 text-xs font-semibold ring-1 ring-inset bg-slate-50 dark:bg-slate-400/10 text-slate-700 dark:text-slate-300 ring-slate-600/10 dark:ring-slate-400/20"
|
||||
>
|
||||
See details
|
||||
{showDetails ? <IconCaretDown size={16} className="ml-1" /> : <IconCaretRight size={16} className="ml-1" />}
|
||||
</button>
|
||||
{showDetails && (
|
||||
<div className="flex relative flex-col text-xs max-w-[364px] max-h-[300px] overflow-scroll mt-2 p-2 bg-slate-50 dark:bg-slate-400/10 ring-1 ring-inset rounded text-slate-700 dark:text-slate-300 ring-slate-600/20 dark:ring-slate-400/20">
|
||||
<span className="font-semibold flex items-center">
|
||||
Impacted Collections: {Object.keys(translationLog || {}).length}
|
||||
</span>
|
||||
<span className="font-semibold flex items-center">
|
||||
Impacted Lines:{' '}
|
||||
{Object.values(translationLog || {}).reduce(
|
||||
(acc, curr) => acc + (curr.script?.length || 0) + (curr.test?.length || 0),
|
||||
0
|
||||
)}
|
||||
</span>
|
||||
<span className="my-1">
|
||||
The numbers after 'script' and 'test' indicate the line numbers of incomplete translations.
|
||||
</span>
|
||||
<ul>
|
||||
{Object.entries(translationLog || {}).map(([name, value]) => (
|
||||
<li key={name} className="list-none text-xs font-semibold">
|
||||
<div className="font-semibold flex items-center text-xs whitespace-nowrap">
|
||||
<IconCaretRight className="min-w-4 max-w-4 -ml-1" />
|
||||
{name}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
{value.script && (
|
||||
<div className="flex items-center text-xs font-light mb-1 flex-wrap">
|
||||
<span className="mr-2">script :</span>
|
||||
{value.script.map((scriptValue, index) => (
|
||||
<span className="flex items-center" key={`script_${name}_${index}`}>
|
||||
<span className="text-xs font-light">{scriptValue}</span>
|
||||
{index < value.script.length - 1 && <> - </>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{value.test && (
|
||||
<div className="flex items-center text-xs font-light mb-1 flex-wrap">
|
||||
<span className="mr-2">test :</span>
|
||||
{value.test.map((testValue, index) => (
|
||||
<div className="flex items-center" key={`test_${name}_${index}`}>
|
||||
<span className="text-xs font-light">{testValue}</span>
|
||||
{index < value.test.length - 1 && <> - </>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button
|
||||
className="absolute top-1 right-1 flex w-fit items-center rounded p-2 text-xs font-semibold ring-1 ring-inset bg-slate-50 dark:bg-slate-400/10 text-slate-700 dark:text-slate-300 ring-slate-600/10 dark:ring-slate-400/20"
|
||||
onClick={(e) => copyClipboard(e, JSON.stringify(translationLog))}
|
||||
>
|
||||
<IconCopy size={16} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, translationLog }) => {
|
||||
const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -150,9 +53,6 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
|
||||
Name
|
||||
</label>
|
||||
<div className="mt-2">{collectionName}</div>
|
||||
{translationLog && Object.keys(translationLog).length > 0 && (
|
||||
<TranslationLog translationLog={translationLog} />
|
||||
)}
|
||||
<>
|
||||
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
|
||||
Location
|
||||
@@ -161,7 +61,6 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full cursor-pointer"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
@@ -169,6 +68,9 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
onChange={e => {
|
||||
formik.setFieldValue('collectionLocation', e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
|
||||
@@ -14,7 +14,7 @@ import Dropdown from "components/Dropdown";
|
||||
import { IconCaretDown } from "@tabler/icons";
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const NewFolder = ({ collection, item, onClose }) => {
|
||||
const NewFolder = ({ collectionUid, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const [isEditing, toggleEditing] = useState(false);
|
||||
@@ -52,7 +52,7 @@ const NewFolder = ({ collection, item, onClose }) => {
|
||||
})
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(newFolder(values.folderName, values.directoryName, collection.uid, item ? item.uid : null))
|
||||
dispatch(newFolder(values.folderName, values.directoryName, collectionUid, item ? item.uid : null))
|
||||
.then(() => {
|
||||
toast.success('New folder created!');
|
||||
onClose();
|
||||
|
||||
@@ -5,7 +5,7 @@ import toast from 'react-hot-toast';
|
||||
import path from 'utils/common/path';
|
||||
import { uuid } from 'utils/common';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { newEphemeralHttpRequest } from 'providers/ReduxStore/slices/collections';
|
||||
import { newHttpRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
@@ -20,9 +20,11 @@ import Portal from 'components/Portal';
|
||||
import Help from 'components/Help';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
|
||||
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
|
||||
const {
|
||||
brunoConfig: { presets: collectionPresets = {} }
|
||||
} = collection;
|
||||
@@ -135,14 +137,14 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
requestType: values.requestType,
|
||||
requestUrl: values.requestUrl,
|
||||
requestMethod: values.requestMethod,
|
||||
collectionUid: collection.uid
|
||||
collectionUid: collectionUid
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uid,
|
||||
collectionUid: collection.uid,
|
||||
collectionUid: collectionUid,
|
||||
requestPaneTab: getDefaultRequestPaneTab({ type: values.requestType })
|
||||
})
|
||||
);
|
||||
@@ -158,7 +160,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
requestType: curlRequestTypeDetected,
|
||||
requestUrl: request.url,
|
||||
requestMethod: request.method,
|
||||
collectionUid: collection.uid,
|
||||
collectionUid: collectionUid,
|
||||
itemUid: item ? item.uid : null,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
@@ -178,7 +180,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
requestType: values.requestType,
|
||||
requestUrl: values.requestUrl,
|
||||
requestMethod: values.requestMethod,
|
||||
collectionUid: collection.uid,
|
||||
collectionUid: collectionUid,
|
||||
itemUid: item ? item.uid : null
|
||||
})
|
||||
)
|
||||
@@ -389,8 +391,6 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
) : (
|
||||
<div className='relative flex flex-row gap-1 items-center justify-between'>
|
||||
<PathDisplay
|
||||
collection={collection}
|
||||
dirName={path.relative(collection?.pathname, item?.pathname || collection?.pathname)}
|
||||
baseName={formik.values.filename? `${formik.values.filename}.bru` : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,21 +11,19 @@ import { useDispatch } from 'react-redux';
|
||||
import { showHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { multiLineMsg } from "utils/common";
|
||||
import { formatIpcError } from "utils/common/error";
|
||||
|
||||
const TitleBar = () => {
|
||||
const [importedCollection, setImportedCollection] = useState(null);
|
||||
const [importedTranslationLog, setImportedTranslationLog] = useState({});
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
const handleImportCollection = ({ collection, translationLog }) => {
|
||||
const handleImportCollection = ({ collection }) => {
|
||||
setImportedCollection(collection);
|
||||
if (translationLog) {
|
||||
setImportedTranslationLog(translationLog);
|
||||
}
|
||||
setImportCollectionModalOpen(false);
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
};
|
||||
@@ -38,9 +36,8 @@ const TitleBar = () => {
|
||||
toast.success('Collection imported successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
console.error(err);
|
||||
toast.error('An error occurred while importing the collection. Check the logs for more information.');
|
||||
toast.error(multiLineMsg('An error occurred while importing the collection.', formatIpcError(err)));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -75,7 +72,6 @@ const TitleBar = () => {
|
||||
{importCollectionLocationModalOpen ? (
|
||||
<ImportCollectionLocation
|
||||
collectionName={importedCollection.name}
|
||||
translationLog={importedTranslationLog}
|
||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||
handleSubmit={handleImportCollectionLocation}
|
||||
/>
|
||||
|
||||
@@ -83,7 +83,7 @@ class SingleLineEditor extends Component {
|
||||
}
|
||||
});
|
||||
}
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
this.editor.setValue(String(this.props.value ?? ''));
|
||||
this.editor.on('change', this._onEdit);
|
||||
this.addOverlay(variables);
|
||||
this._enableMaskedEditor(this.props.isSecret);
|
||||
@@ -107,7 +107,7 @@ class SingleLineEditor extends Component {
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.cachedValue = this.editor.getValue();
|
||||
if (this.props.onChange) {
|
||||
if (this.props.onChange && (this.props.value !== this.cachedValue)) {
|
||||
this.props.onChange(this.cachedValue);
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ class SingleLineEditor extends Component {
|
||||
}
|
||||
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) || '');
|
||||
this.editor.setValue(String(this.props.value ?? ''));
|
||||
}
|
||||
if (!isEqual(this.props.isSecret, prevProps.isSecret)) {
|
||||
// If the secret flag has changed, update the editor to reflect the change
|
||||
@@ -146,7 +146,7 @@ class SingleLineEditor extends Component {
|
||||
|
||||
addOverlay = (variables) => {
|
||||
this.variables = variables;
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', this.props.highlightPathParams);
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', this.props.highlightPathParams, true);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user